SELECT / ResultSet

Jan 30, 2024
SELECT / ResultSet

1. SELECT는 ‘테이블’을 전송한다.

SELECT는 다른 애들과는 달리 전송하는 메소드가 다르다. (executeQuery() 쓴다) (요청하고 나서 테이블을 받기 때문! SELECT는 write가 아니다! 조회할 거다!)
notion image
 

2. ResultSet & next & 프로젝션의 개념

[ ResultSet ]

ResultSet = 애가 바로 테이블 형태의 데이터를 나타내는 자바 객체다 * ResultSet은 커서를 한 칸씩 내리며 데이터를 탐색하는 기능을 제공 * next() 메서드를 호출하면 커서는 다음 행으로 이동하고, 해당 행의 데이터를 읽을 수 있다. 이를 통해 ResultSet 객체는 한 번에 한 행씩 데이터를 처리할 수 있다.
notion image
💡
* 애가 바로 ResultSet! 정보가 이렇게 '테이블 형식'으로 넘어온다! * 커서를 내리는게 중요!
💡
ResultSet = rs = 커서 (git의 헤더와 비슷하다)
 

[ rs.next = 커서를 한 칸 내리기 ]

notion image
GIT의 헤더처럼 rs(ResultSet)는 손가락(커서)같은걸 들고 있다. 이 rs 객체는 처음 생성될 때 첫 번째 행의 이전(즉, 컬럼의 제일 윗칸)을 가리킨다. 이제 next() 메서드를 호출해 커서를 한칸 내리면서 데이터를 읽는다. ex) row가 10개면, 커서를 10번 내려가며 데이터를 읽는다.
 

[ 프로젝션 ]

원하는 컬럼(속성)만 선택하여 조회하는 것. 즉, 원하는 것만 하나씩 찝어내서 읽는 것이다. 현재, 우리가 보고 있는 예시엔 컬럼이 총 4개다. 여기서, 내가 읽고 싶은게 password면 password만 프로젝션하면 애만 읽는다! 따라서, 원하는 컬럼을 선택하여 읽으려면 해당 컬럼을 커서가 가리키도록 설정한 후 데이터를 읽고, 다음 컬럼으로 커서를 이동시키는 작업을 반복하면 된다. 전체 컬럼을 읽으려면 '총 4번' 이동하여 데이터를 읽는 것.
💡
프로젝션은 SELECT 문에서 사용하는 개념
 

2-1. ResultSet & next & 프로젝션 실습

[ MAIN 코드 ]

notion image
notion image
next() 메서드는 return 값이 boolean이다! boolean은 다음 행이 존재하는지 여부를 나타내며, true인 경우 다음 행이 존재하고, false인 경우 다음 행이 없음을 의미 (한 칸 커서를 내렸더니 값이 없을 수도 있으니까...)
notion image
일단 여기까지만 해놓고 test코드에가서 true값이 잘 뜨는지 확인해보자
 

[ Test 코드 ]

notion image
본코드에서 rs.next해서 커서가 한 칸 내려 온 상태잖아? 이제 프로젝션해서 값을 읽어보자 [ int number = 5 ] 즉, 5번 행에 있는 테이블 데이터를 끄집어낼 것이다.
notion image
ResultSet에서 데이터를 읽을 때는 getXXX() 메서드를 사용한다. getXXX() 메서드는 컬럼의 데이터를 가져오는 역할! (XXX에 해당하는 타입을 넣는다) rs.getInt("number") 이렇게 key값에 number을 넣으면 "number"라는 컬럼의 데이터를 int 타입으로 가져오는 메서드 rs.getString("password") "password"라는 컬럼의 데이터를 String 타입으로 가져오는 메서드 rs.getInt("balance") "balance"라는 컬럼의 데이터를 int 타입으로 가져오는 메서드 rs.getTimestamp("created_at") "created_at"이라는 컬럼의 데이터를 Timestamp 타입으로 가져오는 메서드
💡
createdAt 아니고, DB에서 가져오는 것이기에 created_at 테이블의 특정 행에 저장된 데이터 (테이블 데이터)를 끄집어냈죠? select 해서 컬럼 하나를 딱! 찝어왔음!
notion image
notion image
5번 number와 5번 password 5번 등등을 5번에 있는 테이블을 들고 온 결과 확인!!! 이게 바로 프로젝션이다!!! 끄집어내는 것!!!
💡
그런데…. 헷갈리잖아. 번거롭고. 이제 이 프로젝션을 아까 만든 Account에 들고 가서 만들자 ^^
 

2-2. ResultSet을 return값으로 쓰는 건 안될까?

안된다. 예시를 보자. 창고에 쌀이랑 보리가 막 있는데 이걸 포대기에 다 담는다. 막 담았으니 쌀과 보리가 다 섞여있는 상태겠지? 이게 ResultSet! (ResultSet은 데이터베이스에서 실행한 쿼리의 결과를 담고 있는 객체이니...) 이 ResultSet을 DAO를 이용해 하나하나 분리한 후, 예쁘게 정리했다. (DAO는 데이터를 추출하고 가공하여 필요한 형태로 정리하는 역할) 이제 큰 상자를 하나 사서, 칸을 만들고, 거기에 프로젝션 한 걸 다 옮겨담는다. 그 옮겨 닮은 것(결과)을 주는 것!
notion image
그런데 봐... 이렇게 하면... 와! 예쁘게 정리 다 했다! ......다시 막 섞자!!! 하는 것... 그러면 또 rs로 커서 내리고 어쩌고 저쩌고 해야한다 ㅠㅠ
 

3. Account 클래스

왜 클래스에 담을까?

number가 PK로 설정되어 있기 때문에 1건이란 걸 확신할 수 있다. 그렇기에 해당 데이터를 클래스에 담을 수 있다. 클래스에 담자! (=넘버가 PK로 설정되어 있다면, 해당 데이터를 클래스의 인스턴스로 만들어서 담을 수 있다) 즉, 우린 계좌 정보를 담는 Account 클래스를 만들 것이다. 이 클래스의 인스턴스(new)는 각각의 계좌 정보를 나타내는 것. 이렇게 클래스를 사용하여 데이터를 담으면, 각각의 데이터를 관리하고 활용할 수 있다. (데이터를 구조화하고 관리하기 위해서 사용하는 듯) 넘버가 PK로 설정되어 있다면, 각 인스턴스 당 한 개의 계좌 정보를 담을 수 있단 말이잖아? (각 인스턴스가 넘버를 통해 고유하게 식별될 수 있단 것.)

데이터 구조화의 이점

데이터를 구조화하면 데이터의 특성과 관계에 따라 데이터를 분류하고 정리할 수 있다. 계좌 정보를 구조화한다고 가정해자. 각 계좌는 계좌번호, 예금주명, 잔액 등의 정보로 구성된다. 이러한 정보를 구조화하여 Account 클래스로 정의하고, 각 계좌 정보를 해당 클래스의 인스턴스로 생성하여 담을 수 있다. 이렇게 데이터를 구조화하면 데이터를 보다 쉽게 접근하고 관리할 수 있.
 

3-1. Account 클래스에 담아보자

'isRow' 가 true값일 때만 담으면 되죠? (false면 더이상 값이 없다는 뜻이니까...)

[ selectByNumber 전체 코드 ]

public Account selectByNumber(int number){ Connection conn = DBConnection.getInstance(); try { String sql = "select * from account_tb where number = ?"; PreparedStatement pstmt = conn.prepareStatement(sql); pstmt.setInt(1, number); ResultSet rs = pstmt.executeQuery(); boolean isRow = rs.next(); // 커서 한칸 내리기 if(isRow){ Account account = new Account( //Account의 생성자로 담음 rs.getInt("number"), rs.getString("password"), rs.getInt("balance"), rs.getTimestamp("created_at") ); return account; } }catch (Exception e){ e.printStackTrace(); } return null; }
 

[ MAIN 코드 ]

notion image
notion image
2-1에서 설명했던 대로 이 값 4개를 Account 클래스에 담을 것임 그럴려면... Account 클래스에서 생성자를 만들어야겠지. (초기화할거니까!) 생성자는 객체를 생성하는 동시에 속성 값을 전달받아 초기화할 수 있는 특별한 메서드. (객체를 생성할 때 필요한 속성 값을 설정하기 위해서 생성자를 만든다) 위의 코드에서는 Account 클래스에 생성자를 만들었는데, 이 생성자는 number, password, balance, createdAt를 인자로 받아 해당 속성을 초기화한다. 즉, selectByNumber 메서드에서 데이터베이스에서 조회한 결과를 Account 객체로 생성할 때, 생성자를 사용하여 객체를 초기화하려고 생성자를 만들었다! 아무튼 지금 생성자가 없어서 오류가 나니까, Account에 @AllArgsConstructor 롬복 만들어주자
notion image
생성자 만들었다!
notion image
행을 찾으면 찾은 값을 주고(return account), 못찾으면 null을 주고
notion image
return이 있으니, 타입도 return으로 바꿔주고
 

+) 왜 Account 객체를 생성하고 초기화 했을까

selectByNumber 메서드는 특정 계좌 번호를 입력받아서 해당 계좌의 정보를 데이터베이스에서 조회하는 역할을 한다. 조회된 계좌 정보를 담기 위해 Account 객체를 생성하고 초기화한다. 코드에서 ResultSet 객체를 사용하여 데이터베이스에서 조회한 결과를 가져온다. 그리고 getInt, getString, getTimestamp 등의 메서드를 사용하여 각 열의 값을 추출한다. 이렇게 가져온 값들을 사용하여 Account 객체를 생성하고, 해당 객체에 계좌 번호, 비밀번호, 잔액, 생성 일자 등의 정보를 설정. (Account 객체는 조회된 계좌 정보를 담는 역할) selectByNumber 메서드는 계좌 번호를 입력받아서 해당 계좌의 정보를 조회하고, 조회된 정보를 Account 객체에 담아서 반환해야 함. 이렇게 가져온 값들을 사용하여 Account 객체를 생성하고, 해당 객체에 계좌 번호, 비밀번호, 잔액, 생성 일자 등의 정보를 설정한다. 이렇게 생성된 Account 객체는 조회한 계좌 정보를 담고 있게 되며, 최종적으로 메서드에서 반환된다.
테이블 형태로 날아오니까, 그대로 반환을 못하잖아. 파싱해야지. 그래서 테이블 데이터를 자바 객체(클래스)로 받아서 자바로 파싱 한 다음 해당 객체(Account의 new)를 반환하는 듯? Account 객체 자체를 반환?
 

+) 왜 바로 Account로 못줬을까?

notion image
pstmt.executeQuery() 메서드는 데이터베이스에서 실행한 쿼리의 결과를 담고 있는 ResultSet 객체를 반환하므로, 해당 결과를 Account 객체로 바로 할당할 수는 없다.
 

[ Test 코드 ]

notion image
역시 test에서 given, when, then로 나눠서 짜준다. 자, 그런데... 저거 4개 하나하나 다 get 찍어보기 귀찮지 않나? 객체를 실행하면 바로 나올수 있게 하는 것이 뭐다? toString이다!
 
notion image
롬복은 toString도 지원하니 롬복으로 적어주자!
notion image
그럼 이렇게! get을 4개 다 찍어보지 않고, account 변수명만 넣을 수 있다. 깔끔해졌다!
 

4. selectAll() 메소드 채워 넣기

커서의 이동 횟수는 마지막 행까지다. 그러나 마지막 행이 어디인지 알 수 없다. 즉, while을 돌려서 false나올때까지 돌려야 한다 데이터베이스의 account_tb 테이블에서 모든 계좌 정보를 조회하여 Account 객체의 리스트인 accountList에 저장(추가)하고 반환할 것이다. 그럼... Account를 돌리는거니까 같은 타입이 연속적으로 있는 것! 같은 타입이 연속적으로 있으면 vector니까, 컬렉션을 써야지! selectAll은 while로만 바꾸면 코드 끝
💡
여기서 말하는 Vector가 컬렉션 말하는 게 아님. 개념적인 (연속적인 데이터를 벡터라고 함) 것!! 동일한 타입이 연속적으로 5개 있다 = 1,2,3,4,5 가 있으니 벡터 1, 2, 3, 가, 나, 다로 있으면 타입이 달라서 벡터에 못 담음 즉, 이건 Account를 연속적으로 돌리는 거니까 벡터 ㄱㄱ
 

[ MAIN 코드 ]

notion image
* DIP = 추상적인 것에 의존한다, 그래서 List로 작성했다.
notion image
보통 최근에 회원가입한 사람꺼부터 보고싶은 것 아냐? 그래서 order by 이제 역순으로 보여질 것. ex) 네이버 댓글
💡
전체 조회시에는 거꾸로 조회한다 (최신순으로 조회한다. desc 해줘야)
 

 
notion image
이렇게 코드를 쓰면 생성된 account 객체를 담고, 버리고 담고, 버리고함. 왜냐 while이 stack이라 계속 사라지기 때문에 의미가 없다는 말이다. 그러니 담고 컬렉션에다 옮겨야겠지. List<Account> 사용!
notion image
이 코드를 상상 해보자. 이게... 맞는 코드인가? 테이블 데이터를 보고 커서를 한 칸 내렸다고 생각해라 데이터를 담기 위해 박스를 연속적으로 (=컬렉션) 만들어놓았다. 이제 프로젝션해서 account에 옮기고 'accountList.add(account);' 하면 생성된 객체들을 컬렉션 박스에 옮겻다. 그런데 이건 ... 조건 검색한다 > 다시 박스 만들어진다 > 조건 검색 > 다시 박스 만들어 이런 코드다. 계속 초기화하는 코드다ㅠㅠ 이러면 안 된다. List<Account> 코드가 위로 올라가야 한다. * account는 담기기 때문에 사라져도 된다. `accountList.add(account);` 이 코드 덕분에. (while이 스택이라 사라지니까 담은 것)

[ account는 담기기 때문에 사라져도 된다. 설명 ]

현재 코드에서 accountList는 while 루프 내부에서 선언되고 초기화되고 있기 때문에, while 루프를 반복할 때마다 새로운 accountList 객체가 생성되고 데이터가 추가되는 문제가 있다. 이로 인해 이전에 추가된 데이터는 사라질 수 있다. 따라서, accountList는 while 루프 외부에서 선언되고 초기화되어야 한다. 또한, while 루프 내부에서 accountList에 데이터를 추가하면 된다. 이렇게 하면 while 루프를 반복할 때마다 동일한 accountList에 데이터가 추가되며, 이전에 추가된 데이터는 유지된다.
while 루프 내부에서 accountList에 데이터를 추가하는 이유는 while 루프는 ResultSet에서 한 번에 한 줄씩 데이터를 가져오는 작업을 반복한다. 따라서, while 루프 내부에서 rs.next()를 호출하면 다음 줄의 데이터가 있는지 확인하고, 데이터가 있다면 그 데이터를 읽어와서 Account 객체를 생성한다. 그리고 생성된 Account 객체를 accountList에 추가하는 것. 이 작업을 while 루프 외부에서 처리한다면, ResultSet에서 한 줄을 읽을 때마다 Account 객체를 생성하고 accountList에 추가하는 것이 아니라, while 루프를 반복할 때마다 accountList를 초기화하여 새로운 데이터를 저장할 수 있도록 준비하는 것이다. 그렇게 되면 이전에 저장된 데이터는 계속해서 덮어쓰게 된다. 따라서, while 루프 내부에서 accountList에 데이터를 추가함으로써, 각 줄의 데이터를 읽을 때마다 새로운 Account 객체를 생성하고 accountList에 추가하여 모든 데이터를 유지할 수 있게 된다. (추가되는 것도 while문을 돌아야하니까!)
 

 
notion image
while이 끝낫다 = 커서를 다 내렸다 이 코드는 커서 한칸 내리고 프로덕션, 어카운트에 담고, 다시 상자에 담고... 이런 코드!
 

프로젝트를 새로 만들었더니 롬복이 안돼요

notion image
이런 오류가 뜹니다. arguments야 어디 있니...
notion image
디버깅으로 실행했을 때, 아래에 저런 창이 뜨면 클릭! 이후 디버깅으로 실행하면 된다!!
 

[ 방법 2 ]

https://projectlombok.org/setup/gradle
compileOnly 'org.projectlombok:lombok:1.18.30' annotationProcessor 'org.projectlombok:lombok:1.18.30'
이거 하고 코끼리 눌러라
 

SELECT문 전체 과정 정리

notion image
버퍼에 쿼리문이 담겨서 flush 되어 DB에 들어가면, DB는 테이블 데이터를 버퍼에 담는다. 그런데 타입이 달라서 자바에서 사용하려면 파싱을 해야함. 파싱하기 위해 객체를 데이터를 담는 항아리로 만든다. 그게 바로 Account 클래스. Account 클래스는 테이블 데이터를 담는 용도로 사용될 것이다. 프로젝션을 해서 데이터를 가져올 건데, 첫번째 while 루프가 실행될 때, 3번 테이블 데이터가 account에 담겨서 옮겨진다. 이렇게 자바에서 테이블 데이터를 Account 객체에 담는 것. > 이 경우는, 동일한 타입이 연속되었기 때문에 컬렉션. List<Account> 사용 그런데 select 문이 실행되고 최종적으로 반환(return)되는 것은 accountList다. 바로 저, 파란색 네모 그림!!
notion image
바로 이것!!! accountList에는 Account 객체들이 저장되어 있다. 이렇게 Account 객체들을 모아둔 accountList가 최종적으로 반환되어 자바 코드에서 사용될 수 있게 된다.
💡
이런 흐름과 개념을 이해하라. 코드를 보지 말라. 코드는 복붙 ㄱㄱ
 

DB로부터 데이터를 가져와 자바 객체로 파싱하는 순서

1. 데이터베이스와 연결 JDBC 드라이버를 로드하고 데이터베이스 URL, ID, PASSWORD 등 연결 정보 설정 2. SQL 쿼리를 작성하고 실행 데이터베이스로부터 원하는 데이터를 조회하기 위해 SQL 문을 작성하고, PreparedStatement (버퍼)를 사용하여 쿼리 실행 3. ResultSet 객체를 통해 데이터를 읽는다. 쿼리 실행 결과로 ResultSet 객체를 얻게 된다. ResultSet 객체의 next() 메서드를 호출하여 커서를 다음 행으로 이동하고, 각 열에 대한 데이터를 읽을 수 있다. 4. 읽은 데이터를 자바 객체로 파싱 ResultSet에서 읽은 데이터를 자바 객체로 변환하기 위해 필요한 작업을 수행해야 한다. 이 과정에서 데이터의 유형에 따라 적절한 자바 데이터 타입으로 변환하거나, 자체적으로 정의한 클래스나 DTO(Data Transfer Object) 객체에 값을 설정한다. 5. 파싱된 자바 객체를 활용하여 데이터를 처리하거나, 필요한 로직을 수행
 

지금까지 한 것 전체 코드

import dao.BankDAO; import db.DBConnection; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; import java.util.Scanner; public class BankApp { public static void main(String[] args) { //이거 BR로도 만들 수 있다 Scanner sc = new Scanner(System.in); System.out.print("삭제할 계좌번호를 입력해주세요 : "); int number = sc.nextInt(); //DAO한테 위임 BankDAO dao = new BankDAO(); //결과를 int로 안받으면 잘 됐는지 안됐는지 고객이 알 수가 없음 int result = dao.deleteByNumber(number); if (result == 1) { System.out.println("삭제 성공했습니다"); } else { System.out.println("삭제 실패했습니다"); } } }
package dao; import db.DBConnection; import model.Account; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.util.ArrayList; import java.util.List; public class BankDAO { public int deleteByNumber(int number) { Connection conn = DBConnection.getInstance(); try { String sql = "delete from account_tb where number = ?"; PreparedStatement pstmt = conn.prepareStatement(sql); pstmt.setInt(1,number); int num = pstmt.executeUpdate(); return num; } catch (Exception e) { e.printStackTrace(); } return -1; } public int insert(String password, int balance) { Connection conn = DBConnection.getInstance(); try { String sql = "insert into account_tb(password, balance, created_at) values(?, ?, now())"; PreparedStatement pstmt = conn.prepareStatement(sql); pstmt.setString(1,password); pstmt.setInt(2,balance); int num = pstmt.executeUpdate(); return num; } catch (Exception e) { e.printStackTrace(); } return -1; } public int updateByNumber(int balance, int number) { Connection conn = DBConnection.getInstance(); try { String sql = "update account_tb set balance = ? where number = ?"; PreparedStatement pstmt = conn.prepareStatement(sql); pstmt.setInt(1,balance); pstmt.setInt(2,number); int num = pstmt.executeUpdate(); return num; } catch (Exception e) { e.printStackTrace(); } return -1; } //where에 number가 걸릴거다. public Account selectByNumber(int number){ Connection conn = DBConnection.getInstance(); try { String sql = "select * from account_tb where number = ?"; PreparedStatement pstmt = conn.prepareStatement(sql); pstmt.setInt(1, number); ResultSet rs = pstmt.executeQuery(); boolean isRow = rs.next(); // 커서 한칸 내리기 if(isRow){ Account account = new Account( rs.getInt("number"), rs.getString("password"), rs.getInt("balance"), rs.getTimestamp("created_at") ); return account; } }catch (Exception e){ e.printStackTrace(); } return null; } public List<Account> selectAll() { Connection conn = DBConnection.getInstance(); try { String sql = "select * from account_tb order by number desc"; PreparedStatement pstmt = conn.prepareStatement(sql); ResultSet rs = pstmt.executeQuery(); List<Account> accountList = new ArrayList<>(); while (rs.next()){ Account account = new Account( rs.getInt("number"), rs.getString("password"), rs.getInt("balance"), rs.getTimestamp("created_at") ); accountList.add(account); } return accountList; } catch (Exception e) { e.printStackTrace(); } return null; } }
package model; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.ToString; import java.sql.Timestamp; @Getter @AllArgsConstructor @ToString public class Account { private int number; private String password; private int balance; private Timestamp createdAt; }
 

테스트 파일 전체 코드

package dao; import model.Account; import org.junit.jupiter.api.Test; import java.util.List; public class BankDAOTest { @Test public void selectAll_test() { //given //when BankDAO dao = new BankDAO(); List<Account> accountList = dao.selectAll(); System.out.println(accountList.size()); System.out.println(accountList); } @Test public void selectByNumber_test() { int number = 5; BankDAO dao = new BankDAO(); Account account = dao.selectByNumber(number); if (account == null) { System.out.println(number + "로 조회된 값이 없습니다."); } else { System.out.println(account); // System.out.println(account.getNumber()); // System.out.println(account.getPassword()); // System.out.println(account.getBalance()); // System.out.println(account.getCreatedAt()); } } @Test public void deleteByNumber_test() { // given int number = 1; // when BankDAO dao = new BankDAO(); int result = dao.deleteByNumber(number); // then if(result == 1) { System.out.println("삭제 성공"); } else if (result == 0) { System.out.println(number + "번호를 찾을 수 없습니다."); } else { System.out.println("삭제 실패"); } } @Test public void insert_test() { //given String password = "535115"; int balance = 15000; //when BankDAO dao = new BankDAO(); int result = dao.insert(password, balance); //then if(result == 1) { System.out.println("insert 성공"); } else if (result == 0) { System.out.println(password + "와 " + balance + "를 넣을 수 없습니다"); } else { System.out.println("삭제 실패"); } } @Test public void updateByNumber_test() { //given int balance = 100; int number = 4; //when BankDAO dao = new BankDAO(); int result = dao.updateByNumber(balance, number); //then if (result == 1) { System.out.println("update 성공"); } else { System.out.println("업데이트 실패"); } } }
package db; import org.junit.jupiter.api.Test; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DBConnectionTest { @Test public void getInstance_test() { // given = 파라미터 // when Connection conn = DBConnection.getInstance(); //then if(conn == null) { System.out.println("실패"); } else { System.out.println("성공"); } } }
 
 
Share article

codingb