기초단계/JAVA

2022.12.13 JAVA 데이터베이스 입출력

춘핑이 2022. 12. 13. 20:54

20장 데이터베이스 입출력

20.9 데이터 읽기

PreparedStatement를 생성할때 SQL문이 insert update delete일경우에는 executeUpdate()메소드를 호출하지만
데이터를 가져오는 select문일경우에는 executequery()메소드를 호출해야한다. executequery()메소드는 가져온 데이터를 ResultSet에 저장하고 리턴한다.

ResultSet rs = pstmt.executeQuery();
select userid, username, userage from users;


이것이 ResultSet이다.

20.9.1 ResultSet구조

ResultSet은 select문에 기술된 컬럼으로 구성된 행의 집합이다.
제일앞에 있는 컬럼 1,2,3, 이름으로도 구분가능
제일앞행(beforeFirst)과 제일 뒤행(afterLast)은 빈칸임. 최초 커서위치는 비어있는 제일 앞에 잇는 행을 참조한다.
데이터가 존재하는 첫행은 First 마지막행은 Last이다.
커서를 다음행으로 옮기고 읽어야한다.
ResultSet에는 커서를 이동시키는 것이 있는데 이게 next()메소드이다. 만약 값이 있으면 true를 리턴값으로 주고 next()를 실행
이외에도 커서를 위아래로, 지정된행으로 이동가능한 ResultSet이 있다.
absolute(int row)절대적인 그 행수로 이동해라도 가능
afterFirst()첫행전으로 가라 first()데이터있는 첫행으로 가라
위의 rs에 넣은 ResultSet은 아래로만 갈 수 있다.
스크롤이 가능한 ResultSet을 만들수는 있으나 웹에선 데이터를 얻고 버리기때문에 자주 사용하지 않는다.
이걸 하게 되면 만들기 매우 귀찮아짐.
과거에는 서버가 좋지않아 데이터 일부분을 클라이언트에 가져와서 서버와 동기화하는 식으로 했었는데 서버가 좋아져서 거의 쓰지 않는다.
1개의 데이터행만 가져올경우는 if(rs.next())사용 n개의 데이터행을 가져올경우는 while(rs.next())사용
rs의 첫번째 next()가 값이 없으면 데이터가 하나도 안들어있는거다.
primary_key로 했을 경우 없거나 하나일 수 있지만 다른거로 했을 경우엔 여러개 잡힐수도있다.
ResultSet의 순번은 어떻게 정해지느냐? 데이터베이스의 기준이 아닌 select문에 넣은 순서대로 순번이 정해진다.
select userid, username, userage from users; 이라면 각각 1,2,3 인거고 순서가 바뀌면 번호도 바뀐다.
ResultSet을 다 사용하고 나서는 close()메소드를 호출해서 메모리를 해제해야한다.

20.9.2 데이터행 읽기

커서가 있는 데이터행에서 각 컬럼의 값은 Getter메소드로 읽을수 있다.
컬럼의 데이터타입에 따라 메소드가 결정되며 매개값으로 컬럼의 이름 또는 순번을 줄 수 있다.
String userId = rs.getString("userid") or String userId = rs.getString(1);
int userAge = rs.getInt("userage") or int userAge = rs.getInt(3);

만약 select연산식이나 함수 호출이 포함되어 있다면 컬럼이름대신에 컬럼 순번으로 읽어야한다.
select userid, userage -1 from users
int userAge = rs.getInt(2) userage - 1은 컬럼이름이 아니기때문에 번호로 가져와야한다.

20.9.3 사용자 정보읽기

한행을 어떻게 가져올까?
하나씩 가져올지 하나의 객체로 올지 유저객체로 가져오는게 효율적인가?
데이터베이스의 하나의행은 자바 하나의 객체로 가져오는게 더 효율적임 =객체 매핑
반대로도 하나의 객체를 하나의 행으로 왓다갓다할 줄알아야함.
전자는 데이터가 많으면 많을수록 불리해짐
행이 3개면 객체만 3개만들어서 List에 담아서 사용하면된다.

결론 읽고나서 User객체에 담자.
select userid, username, userpassword, userage, useremail from users where userid= 'winter';
select문에는 연산식도 올수있고 where에도 여러 조건식이 올수잇다.
이것을 또 조건절의 값을 ?로 대체한 매개변수화된 SQL문을 String 변수타입 sql에 대입
String sql = "select userid, username, userpassword, userage, useremail " + "from users " + "where userid= ?";
매개변수화된 select문을 실행하기 위해 prepareStatement()메소드로부터 PreparedStatment를 얻고 ?에 값을 저장한다.
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1,"winter");

executeQuery()메소드로 select문을 실행해서 ResultSet을 얻는다. userid는 기본키이므로 조건에 맞는 행은 1개이거나 0개이다.
if문을 이용해서 next()메소드가 true를 리턴할 경우는 데이터 행을 User객체에 저장하고 출력한다.

ResultSet rs = pstmt.executeQuery();
if (rs.next()) { //1개의 데이터행을 가져왓을경우
User user = new User();
user.setUserId(rs.getString("userid"));
user.setUserName(rs.getString("username"));
user.setUserPassword(rs.getString("userpassword"));
user.setUserAge(rs.getInt(4));//컬럼순번을 이용해서 컬럼지정
user.setUserEmail(rs.getString(5));//컬럼순번을 이용해서 컬럼지정
System.out.println(user);
} else { //데이터 행을 가져오지 않앗을경우
System.out.println("사용자 아이디가 존재하지않음");
}

system.out.println(user)는 롬복이 생성한 User객체의 toString() 메소드를 호출해서 받은 리턴값을 보여준다.

package ch20.sec09.exam01;

import lombok.Data;

@Data // 생성자 게터 세터 hashCode() equals() toString()자동생성 롬복사용안하면 getter와 setter는 꼭 넣어줘야한다.
public class User {
private String userId;
private String userName;
private String userPassword;
private int userAge;
private String userEmail;
}

//DB작업
//매개변수화 된 SQL문 작성
String sql = "select userid, username, userpassword, userage, useremail " + "from users " + "where userid= ?";

//PreparedStatement 얻기 및 값 지정 
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1,"fall");

//userid가 기본키라 하나밖에 못가져온다는 것을 이미 안다.
ResultSet rs = pstmt.executeQuery();
/*
//컬럼하나씩 데이터로 가져와서 읽기 컬럼이 5개뿐이라 짧아보이지만 더 길어지면 힘듦.
if (rs.next()) {
    String userId = rs.getString("userid");
    String userName = rs.getString("username");
    String userPassword = rs.getString("userpassword");
    int userAge = rs.getInt(4);
    String userEmail = rs.getString(5);
printUser(userId, userName, userPassword, userAge, userEmail);
}
*/
//행을 객체로 가져와서 매핑하기
if (rs.next()) { //1개의 데이터행을 가져왓을경우
    User user = new User();
    user.setUserId(rs.getString("userid"));
    user.setUserName(rs.getString("username"));
    user.setUserPassword(rs.getString("userpassword"));
    user.setUserAge(rs.getInt(4));//컬럼순번을 이용해서 컬럼지정
    user.setUserEmail(rs.getString(5));//컬럼순번을 이용해서 컬럼지정
    printUser(user); //System.out.println(user);가 있는 메소드임.

} else { //데이터 행을 가져오지 않앗을경우 
    System.out.println("사용자 아이디가 존재하지않음");
}
rs.close();
pstmt.close();

20.9.4 게시물정보읽기

select문은 다음과 같다. "SELECT bno, btitle, bcontent, bwriter, bdate, bfilename, bfiledata FROM boards WHERE bwriter= 'winter';
조건절의 값을 ?로 바꾸고 sql에 대입
String sql = "SELECT bno, btitle, bcontent, bwriter, bdate, bfilename, bfiledata " + "FROM boards " + "WHERE bwriter=?";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1,"winter");

executeQuery()메소드로 select 문을 실행하서 ResultSet을 얻는데 조건의 맞는행이 n개이므로 while문을 이용해서 반복해야한다.
SQL 문 실행 후, ResultSet을 통해 데이터 읽기
ResultSet rs = pstmt.executeQuery();
while(rs.next()) {
//데이터 행을 읽고 Board 객체 생성
Board board = new Board();
board.setBno(rs.getInt("bno"));
board.setBtitle(rs.getString("btitle"));
board.setBcontent(rs.getString("bcontent"));
board.setBwriter(rs.getString("bwriter"));
board.setBdate(rs.getDate("bdate"));
board.setBfilename(rs.getString("bfilename"));
board.setBfiledata(rs.getBlob("bfiledata"));
//콘솔에 출력
System.out.println(board);
리스트에 담아서 읽기도 가능
List<Board> boards = new ArrayList<>();
boards.add(board);
객체로 불러오고 리스트자체에 넣어버리기도 가능하다.
printBoards(boards); 프린트하는 메소드 작성하고 리스트 자체를 넘겨버리고 프린트했다.
리스트 자체 넘기기 는 웹에서 아주 잘 사용되니 잘알아두자

package ch20.sec09.exam02;

import java.sql.Blob;
import java.util.Date;
import lombok.Data;

@Data
public class Board {
private int bno;
private String btitle;
private String bcontent;
private String bwriter;
private Date bdate;
private String bfilename;
private Blob bfiledata;
}

 //DB작업
//매개변수화 된 SQL문 작성
String sql = "SELECT bno, btitle, bcontent, bwriter, bdate, bfilename, bfiledata " + "FROM boards " + "WHERE bwriter=?";

//PreparedStatement 얻기 및 값 지정 
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1,"winter");

ResultSet rs = pstmt.executeQuery();
//리스트에 담아서 읽기도 가능하다
List<Board> boards = new ArrayList<>();
while(rs.next()) {
    Board board = new Board();
    board.setBno(rs.getInt("bno"));
    board.setBtitle(rs.getString("btitle"));
    board.setBcontent(rs.getString("bcontent"));
    board.setBwriter(rs.getString("bwriter"));
    board.setBdate(rs.getDate("bdate"));
    board.setBfilename(rs.getString("bfilename"));
    board.setBfiledata(rs.getBlob("bfiledata"));    

//파일로 저장
    Blob blob = board.getBfiledata();
        if(blob != null) {
        InputStream is = blob.getBinaryStream();
        OutputStream os = new FileOutputStream("C:/Temp/" + board.getBfilename());
        is.transferTo(os);
        os.flush();
        os.close();
        is.close();
    }
    //내부반복자도 이용가능
    //boards.stream().forEach(b -> System.out.println(b));
    //콘솔에 출력
    //System.out.println(board);
    boards.add(board);
    }
    rs.close();
    printBoards(boards);        
    pstmt.close();

Board의 bfiledata는 Blob객체이므로 콘솔에 출력하면 oracle.sql.BLOB@1972e513와 같이 의미없는 타입정보만 출력된다.
Blob객체의 저장된 바이너리 데이터를 얻기 위해서는 스트림또는 배열을 얻어내야한다.
//파일로 다시 얻어내기, 읽고 다시 다른 프로그램으로 전달하고자 할 때
BLOB blob = board.getBfiledata();
Inputstream is = blob.getBinaryStream();

//바이트배열로 만들기 UI가있으면 거기에 그림을 그리기
BLOB blob = board.getBfiledata();
byte[] bytes = blob.getBytes(0,blob.length()); //0사이즈부터 blob의 사이즈만큼 얻기
InputStream is = blob.getBinaryStream();
OutputStream os = new FileOutputStream("C:/Temp/" + board.getBfilename()); //temp폴더 밑에 파일네임과 동일한 이름으로
is.transferTo(os); //transferTo()메소드는 인풋스트림을 바로 아웃풀스트림으로 보내버리는 메소드이다.
os.flush(); os.close(); is.close();

20.10 프로시저와 함수 호출

프로시저와 함수는 DB에 저장된 프로그램이다.
함수는 메소드와 비슷한것 메소드는 객체내부 기능정리한것 함수는 객체와 상관없이 실행코드를 묶어놓은것임.
제어문 변수 sql문등을 결합해서 실행가능한 코드임.
이걸 어떤 언어로 만드느냐? PL/SQL로 만드는데 이것은 오라클에서만 사용하고 다른데선 작성방법이 좀 다르다.
SQL Developer에서 프로시저

create or replace PROCEDURE user_create (
    a_userid        IN  users.userid%TYPE, 
    a_username      IN  users.username%TYPE,
    a_userpassword  IN  users.userpassword%TYPE,
    a_userage       IN  users.userage%TYPE,
    a_useremail     IN  users.useremail%TYPE,
    a_rows          OUT PLS_INTEGER
) 
IS
BEGIN
INSERT INTO users (userid, username, userpassword, userage, useremail)
VALUES (a_userid, a_username, a_userpassword,  a_userage, a_useremail);
a_rows := SQL%ROWCOUNT;
COMMIT;
END;

외부에서 데이터를받아 BEGIN과 END사이를 실행함. ()사이가 메소드의 매개변수 메소드의 실행부가 BEGIN END;사이라고 생각하면된다.
SQL Developer에서 함수

create or replace FUNCTION user_login (
a_userid        users.userid%TYPE, 
a_userpassword  users.userpassword%TYPE
) RETURN PLS_INTEGER
IS
v_userpassword users.userpassword%TYPE;
v_result PLS_INTEGER;
BEGIN
SELECT userpassword INTO v_userpassword
FROM users
 WHERE userid = a_userid;

IF v_userpassword = a_userpassword THEN
RETURN 0;
ELSE
RETURN 1;
END IF;
EXCEPTION
WHEN NO_DATA_FOUND THEN 
RETURN 2;
END;

함수도 비슷하게 보인다.

함수와 프로시저의 차이점은?
일반적으로 프로시저는 실행하고 끝나고 함수는 결과값을 리턴받아야한다.
개념적으로는 그렇지만 사실 둘다 실행결과를 리턴할 수 있음.
함수는 단일값을 전달하지만 프로시저는 복잡한 구조의 데이터를 전달할 수 있다.
프로시저는 업무처리 로그인하기, 장바구니에담기 주문내역보기 등에 사용 함수는 보통 연산할때 사용
프로시저는 리턴값이 있어도 없어도되고 함수는 반드시 있어야함.
이것을 실행은 DB안에서 실행한다. 이것을 요청을 어디서하나? 이거는 클라이언트쪽에서 요청하거나 다른 프로시저에서도 호출 할 수 있다.
10절에서는 클라이언트 프로그램에서 호출하는 방법을 알아보고자 한다.

JDBC에서 프로시저와 함수를 호출할때는 CallableStatement를 사용한다. 프로시저와 함수의 매개변수화된 호출문을 작성하고
Connection의 prepareCall()메소드로부터 CallableStatement 객체를 얻는다.

프로시저를 호출할 경우
String sql = "{call 프로시저명(?, ?, ..) }";
CallableStatement cstmt = conn.prepareCall(sql);
함수를 호출할 경우
String sql = "{? = call 함수명(?, ?, ..) }";
CallableStatement cstmt = conn.prepareCall(sql);

둘다 중괄호로 감싼 call문이라는 점은 동일하지만 함수는 call문의 실행결과를 대입할 좌측 리턴값 자리(?=)을 명시해야한다.
프로시저명과 함수 명 괄호안에 작성된 ?는 호출시 필요한 매개값의 자리이다.
주의할점은 프로시저도 리턴값과 유사한 OUT타입의 매개변수를 가질 수 있기 때문에 괄호안의?중일부는 OUT값(리턴값)일수 있다는 점이다.
prepareCall()메소드로 CallableStatement을 얻엇다면 리턴값에 해당하는 ?는 registerOutParameter()메소드로 지정하고
그외의 ?는 호출시 필요한 매개값으로 Setter메소드를 사용해서 값ㅇ르 지정해야한다.
함수는 첫번째?가 무조건 리턴값이다.
프로시저
csmt.setString(1,"값"); //프로시저의 첫번째 매개값
csmt.registerOutParameter(2, 리턴타입) // 프로시저의 두번째 ?을 OUT값(리턴값)임을 지정
자바에서는 매개변수가 전부다 in값이지만 프로시저에서는 매개변수에 out값도 있다. out은 프로시저의 결과값이라고 보면된다.
함수
csmt.registerOutParameter(1, 리턴타입) // 첫번째 ?는 리턴값임을 지정
csmt.setString(2,"값"); //함수의 첫번째 매개값
?에 대한 설정이 끝나면 프로시저 또는 함수를 호출하기 위해 execute()메소드를 호출한다.
csmt.execute();
호출후에는 Getter메소드로 리턴값을 얻을 수 있다. 리턴 타입이 정수라고 가정하면 프로시저와 함수의 리턴값은 다음과같다.
프로시저 int result = cstmt.getInt(3); 함수 int result = cstmt.getInt(1);
더이상 사용하지 않는다면 close()함수를 호출해서 메모리를 해제해야한다.

20.10.1 프로시저 호출

위의 프로시저를 가져오면 이런식으로 구성되어있다.
create or replace PROCEDURE user_create (
a_userid IN users.userid%TYPE, //userid의 컬럼과 같은 타입으로 해라
...
a_rows OUT PLS_INTEGER

String sql = "{call user_create(?, ..., ?) }";
CallableStatement cstmt = conn.prepareCall(sql);
이 호출문에서 앞에까지는 매개값기오 마지막이 리턴값이다. 따라서 다음과 같이 ?의 값을 지정하고 리턴값으로 저장한다.
cstmt.setString(1,"summer");
...
cstmt.regisetOutParameter(6, Types.INTEGER);
SQL타입이나 JDBC타입을 사용해야하는데 java.sql.Types에 상수로 들어있다.
리턴타입은 Types의 상수인 INTEGER로 지정하였는데 그 이유는 user_create프로시저의 6번째 매개변수값이 PLS_INTEGER이기때문이다.
실제 실행하면 BEGIN END사이의 문이 실행된다.
위에서보면 INSERT INTO문이 실행되고 마지막에 a_rows := SQL%ROWCOUNT; 이렇게되어있는데 :=은 우측값을 좌측에 대입하는 것인데 바로직전에 실행한 행의 수 반환임.
user_create을 실행하고 리턴값을 얻는다 성공적으로 저장되면 a_rows := SQL%ROWCOUNT;때문에 1이 리턴된다.
csmt.execute();
int rows = cstmt.getInt(6); //6번째 ?값얻기

//DB작업
//매개변수화 된 SQL문 작성
String sql = "{call user_create(?, ?, ?, ?, ?, ?) }";
//CallableStatement 얻기 및 값 지정 
CallableStatement cstmt = conn.prepareCall(sql);

cstmt.setString(1, "summer");
cstmt.setString(2, "한여름");
cstmt.setString(3, "12345");
cstmt.setInt(4, 26);
cstmt.setString(5, "summer@mycompany.com");
cstmt.registerOutParameter(6, Types.INTEGER);

cstmt.execute();
int rows = cstmt.getInt(6);
System.out.println("저장된 행수: " + rows);    
cstmt.close();

20.10.2 함수호출

user_login()함수는 2개의 매개변수와 PLS_INTEGER리턴타입으로 구성되어있다.
String sql = "{? = create or replace FUNCTION user_login (? ,?)}";
앞의 ?인 리턴값이 RETURN PLS_INTEGER이다.
BEGIN과 END사이를 보면
로그인이란것은 사용자로부터 ID를 받고 userpassword를 비교하는 것이다.
SELECT userpassword INTO v_userpassword 라고 나와있는 것은 userpassword를 가져와서(into) v_userpassword 저장해라는뜻
v_userpassword와 사용자가 입력한 a_userpassword와 값을 비교
맞으면 0값리턴 패스워드 틀리면 1값
예외발생시? NO_DATA_FOUND id가 없으면 예외가 발생하니 2값 리턴
이걸로 리턴값지정, 값지정은 프로시저와 같다.
출력값은 int result = cstmt.getInt(1); 첫번째 ?값얻기 0|1|2중하나임.

//DB작업
//매개변수화 된 SQL문 작성
String sql = "{ ? = call user_login(?, ?) }";
//CallableStatement 얻기 및 값 지정 
CallableStatement cstmt = conn.prepareCall(sql);

cstmt.registerOutParameter(1, Types.INTEGER);
cstmt.setString(2, "winter");
cstmt.setString(3, "12345");

cstmt.execute();
int result = cstmt.getInt(1);

//이거대신 if문사용해도됨.
String message = switch(result){
    case 0 -> "로그인성공";
    case 1 -> "비밀번호가 틀림";
    default -> "아이디가 존재하지 않음";
};
System.out.println(message);

cstmt.close();

직접 insert문 쓴거랑 프로시저 쓴것의 차이
결국 DB에 전송해서 SQL문을 처리하는데 전송량이 전자가 더많고 프로시저가 더 적다.
그래서 프로시저가 더빠르게 실행된다. 그래서 이것이 더 유리하다.
인서트문은 SQL문을 컴파일해서 실행해야한다. 프로시저는 이미 컴파일이 되어있다.
이미 컴파일된거를 실행하는것이 속도가 더 빠르다.
현업에서는 속도, 보안의 이유로 프로시저를 사용한다.
보안? insert문에는 어떤 컬럼이 있는지 다 드러난다. 그래서 유출되면 테이블 구조가 유출된다.
그런데 요즘엔 전자를 더많이쓰기도한다.
근데 모든 걸 다 insert문에 쓰지않는데 이것은 ORM이 이를 해결해준다.
ORM?
JAVA는 개별적인 행이 아닌 이것을 DTO객체로 매핑해서 만들고 보내고자 한다.
ORM은 이 기능을 제공하는 프로그램, 라이브리러리이다.
반대로 DTO객체를 행으로 저장해주는 것도 가능하다.
ORM은 종류가 많고 프로젝트할때 대부분 이것을 사용한다. 이걸 사용하면 insert select문을 직접 사용하는 것이 더 편하다.
요즘엔 DB가 잘만들어져잇어서 뭘 사용하던 차이가 없긴하다. 개발할때는 편리성을 고려해서 맞는 것을 사용한다.
결론적으로 ORM내부에서는 단편적인 sql문을 더 많이 사용한다.

20.11 트랜젝션 처리

우리나라말로 대체 불가능
트랜젝션은 기능 처리의 최소 단위를 말한다.
하나의 기능은 여러가지 소작업들로 구성된다. 최소단위라는 것은 이 소작업들을 분리할 수 없으며 전체를 하나의 본다는 개념
트랜젝션은 소작업들이 모두 성공하거나 실패해야한다.


계좌이체는 출금과 입금이 동시에 이루어져야 한다.
출금계죄에서 금액을 감소시키고 입금계좌에서 금액을 증가시키낟. 따라서 다음과 같이 두개의 UPDATE문이 필요하고 둘다 성공해야한다.
출금작업 UPDATE accounts SET balance=balance-이체금액 WHERE ano=출금계좌번호
입금작업 UPDATE accounts SET balance=balance+이체금액 WHERE ano=입금계좌번호

트랜젝션을 처리하기 위해 커밋과 롤백을 제공한ㄷ. 커밋은 내부작업을 모두 성공처리하고 롤백은 실행전으로 돌아간다는 의미에서 모두 실패처리한다.
JDBC에서는 INSERT UPDATE DELETE문을 실행할때마다 자동커밋이 일어난다. 이 기능은 두가지 UPDATE문을 실행할때 문제가 된다.
출금 작업이 성공되면 바로커밋되기때문에 입금작업의 성공여부와 상관없이 출금작업만 별도처리된다.

따라서 JDBC에서 트랜젝션을 코드로 제어하려면 자동 커밋 기능을 꺼야한다. 저동 커밋 설정여부는 Connection의 setAutoCommet()메소드로 할 수 있다.
conn.setAutoCommit(false);
자동커밋기능이 꺼지면 conn.commit(); conn.rollback();처럼 커밋과 롤백을 제어할 수 있다.
일반적인 코드 작성패턴은 다음과같다.
트랜젝션시작--
연결
자동커밋끄기conn.setAutoCommit(false);
소작업처리
소작업처리
커밋->모두 성공처리
트랜젝션 종료
catch문
롤백 -> 모두실패처리
finally
conn.setAutoCommit(true); 끊기전에 자동커밋 켜기
연결끊기
지금은 다시 켜줄 필요가 없지만 실제 프로젝트에서는 커넥션풀을 사용하기 때문에 다시 키는게 필요하다.
커넥션풀이란?
연결속도를 단축하는 것이다. db와 연결하는 시간이 오래걸리는데 커넥션풀은 미리 연결해놓앗다가 필요할때 제공하므로 속도가 빨라진다.
connection Pool에서 conn.close()를 하면 connection을 반환하는 것이다.
반납할땐 돌려줘야하니 다시 자동커밋을 키고 보내는 것이다.

//트랜잭션시작------------------------
//DB작업
//자동 커밋 기능 끄기
conn.setAutoCommit(false);

//출금 작업
String sql1 = "UPDATE accounts SET balance=balance-? WHERE ano=?";
PreparedStatement pstmt1 = conn.prepareStatement(sql1);
pstmt1.setInt(1,  10000);
pstmt1.setString(2, "111-111-1111");
int rows1 = pstmt1.executeUpdate();
if(rows1 == 0) throw new Exception("출금되지 않았음");
//실패하면 예외를 발생시킴 -> catch문으로 보내버림 그냥 Exception했는데 사용자정의예외만드는게 best임
pstmt1.close(); //이게닫힌다고 커밋을 안해서 연결이 끊기는게 아님.

//입금 작업
String sql2 = "UPDATE accounts SET balance=balance+? WHERE ano=?";
PreparedStatement pstmt2 = conn.prepareStatement(sql2);
pstmt2.setInt(1,  10000);
pstmt2.setString(2, "222-222-2222");
int rows2 = pstmt2.executeUpdate();
if(rows2 == 0) throw new Exception("입금되지 않았음");
pstmt2.close();

//수동 커밋 -> 모두 성공 처리
conn.commit();
System.out.println("계좌 이체 성공");    

에러

String sql = "select userid, username, userpassword, userage, useremail공백" + "from users공백" + "where userid= ?";
sql작성할때 마지막에 꼭 띄어쓰기해주자! 문자열을 합칠때 단어가 합쳐저버려서 오류가 발생한다.