스프링 DB 1편

Seung jun Cha·2022년 11월 5일
0

1. JDBC

1-1 개념

  • 자바에서 데이터베이스마다 다른 연결방법, SQL응답 등을 표준화하여 DB를 사용할 수 있도록 하는 자바 표준 API. 각 DB에 맞는 드라이버를 만들어 놓았고 DB에 맞는 드라이버를 설치하면 된다. 즉, 개발자는 JDBC표준 인터페이스에만 맞게 구현하고 드라이버만 바꾸면 된다. 그러나 SQL은 DB에 맞게 변경해야한다.**
        Connection conn = null;

        try {
            DriverManager.registerDriver(new org.h2.Driver());
            //이거 안적어줘도 등록되어 있는 드라이버를 인식 자동으로 인식하기는 함
            
            conn = DriverManager.getConnection("jdbc:h2:~/JDBC", "sa", "");
        } catch (SQLException e) {
            e.printStackTrace();
        }

        return conn;
    }
  1. JDBC가 제공하는 DriverManager는 라이브러리에 등록된 드라이버들을 관리하고 커넥션을 가져오는 기능을 제공한다.

  1. 2개 이상의 DB 드라이버가 등록되어 있는 경우에 커넥션을 요청하면 모든 드라이버에게 커넥션 요청이 가고, 각각의 드라이버들은 커넥션정보(URL, 사용자명, 비밀번호) 중 URL을 체크해서 자신이 처리할 수 있는 URL인지 판단하고 커넥션을 반환한다.

1-2 구성

1-2-1 JDBC Driver

  1. 드라이버가 JDBC 인터페이스를 구현하고 DB를 작동시켜 SQL을 던지고 결과를 받음

  2. 드라이버를 통해 DB에 접근할 수 있는 커넥션을 얻을 수 있음
    매니저로 커넥션 객체를 가져옴(url, 유저네임 , 패스워드) - 데이터베이스와 연결

1-2-2 JDBC API

  1. Connection : 특정 데이터베이스와 연결 객체로 DB와 이루어지는 기능들은 이 객체를 사용함

  2. Statement : SQL문을 실행해 작성한 결과를 담는 곳으로 먼저connection을 사용해서 생성해야 함

    public void insert(Person person) {
        String query = "INSERT INTO PERSON(id, name) " 
    			+ "VALUES(" + person.getId() + ", '" + person.getName() + "')";
    
        Statement statement = connection.createStatement();
        statement.executeUpdate(query);
    }
  3. PreparedStatement : sql문을 파라미터 없이 미리 컴파일해서 성능개선하고 이후에 파라미터 세팅

    public void insert(Person person) {
        String query = "INSERT INTO PERSON(id, name) VALUES(?, ?)";
    
        PreparedStatement preparedStatement = connection.prepareStatement(query);
        preparedStatement.setInt(1, person.getId());
        preparedStatement.setString(2, person.getName());
        preparedStatement.executeUpdate();
    }
  4. ResultSet - SQL문에 대한 결과를 저장하는 곳, 내부에 있는 자료를 가르키는 커서를 통해 next() 함수로 데이터를 가지고 온다.

 public Member findById(String memberId){
        String sql = "select * from member where member_id = ?";

        Connection connection = null;
        PreparedStatement preparedStatement = null;
        ResultSet rs = null;

        try {
            connection = getConnection();
            connection.prepareStatement(sql);
            preparedStatement.setString(1, memberId);
            rs = preparedStatement.executeQuery();
            
            if (rs.next()){  // 선택한 멤버의 값을 새 멤버에 세팅
                Member member = new Member();
                member.setMemberId(rs.getString("memberId"));
                member.setMoney(rs.getInt("money"));
                return member;
            }else {
                throw new NoSuchElementException
                ("member not found memberId=" + memberId);
            }

        } catch (SQLException e) {


        }finally {
           close(connection, preparedStatement, rs);
        }
    }
  1. executeQuery() : select문을 실행해서 select된 데이터가 담근 Resultset을 반환

  2. executeupdate : DML(insert,update,delete)를 실행하고 업데이트 된 row의 개수를 반환

1-2-3 DB와 연결하는 방법

  • Statement 또는 PreparedStatement
  1. JDBC 드라이버 인스턴스 생성

    DriverManager.*registerDriver*("드라이버 객체");`
  2. JDBC 드라이버 인스턴스를 통해 DBMS에 대한 연결 생성

    Connection conn = DriverManager.getConnection("URL", "user", "password");
  3. Statement 생성 또는 PreparedStatement 생성

      conn.createStatement(); // Statement는 완성된 쿼리문을 담음
      
      // PreparedStatement
    PreparedStatement stmt = conn.prepareStatement("완성되지 않은 SQL");`
    `stmt.setString(1, 인자값 1);`
    `stmt.setString(2, 인자값 2);`
    `stmt.setInt(3, 인자값 3);
  1. 질의문을 실행하고 ResultSet / int 결과 받음
ResultSet rs = stmt.executeQuery(”완성된 SELECT SQL”);
int rs = stmt.executeUpdate(”완성된 INSERT/UPDATE/DELETE SQL”);
  1. ResultSet Statement Connection를 close
      rs.close(); , stmt.close();`\ connet.close();

1-2-4 DAO

  • Data Access Object : 서블릿과 DB 사이에서 서로를 연결
    데이터를 가지고 올 때는 DB에 접근해서 SQL을 실행하고 실행 결과를 자바 객체에 맵핑하며 데이터를 저장할 떄는 SQL로 DB에 Record단위로 저장한다

2. 커넥션 풀과 데이터소스

2-1 DataSource

  • DataSource는 커넥션을 획득하기 위한 방법을 추상화한 인터페이스를 말한다(드라이버매니저를 사용할지, Hikari를 사용할지 등..)
    커넥션이 생성되는 과정은 이렇다
  1. 애플리케이션 로직은 DB 드라이버를 통해 커넥션을 조회한다.

  2. DB 드라이버는 DB와 TCP/IP 커넥션을 연결한다.

  3. DB 드라이버는 TCP/IP 커넥션이 연결되면 ID, PW와 기타 부가정보를 DB에 전달한다.

  4. DB는 ID, PW를 통해 내부 인증을 완료하고, 내부에 DB 세션을 생성한다.

  5. DB는 커넥션 생성이 완료되었다는 응답을 보낸다.

  6. DB 드라이버는 커넥션 객체를 생성해서 클라이언트에 반환한다.

이러면 매번 커넥션을 생성해야 하기 때문에 시간이 오래걸린다. 그래서 나온 것이 커넥션 풀인데 아래에서 설명한다.

  • 스프링은 DriverManager도 DataSource를 통해서 커넥션을 획득할 수 있도록 DriverManagerDataSource라는 DataSource를 구현한 클래스를 제공한다
DriverManagerDataSource driverManagerDataSource = 
new DriverManagerDataSource(URL, USERNAME, PASSWORD);

Connection connection = driverManagerDataSource.getConnection();
  • 처음에 DataSource를 만들때만 정보를 등록하고 커넥션을 가지고 올 때는 그냥 가지고 오면 된다. 설정과 사용을 분리함으로써 이후 변경에 더 유연하게 대처할 수 있게 된다. 그러나 DriverManagerDataSource 방법은 설정과 사용을 분리하는 것일뿐 커넥션 풀을 사용하는 방법은 아니다.

2-2 커넥션 풀

  • 데이터베이스 연결을 미리 생성하여 관리하는 기술로 애플리케이션 시작 시 미리 정의된 개수의 데이터베이스 연결을 생성한다. 커넥션이 필요할 때는 DB에서 커넥션을 받는 것이 아니라 이미 만들어진 커넥션을 커넥션 풀에서 가져다가 쓰고 종료하지 않은 상태로 풀에 반환한다. 커넥션 풀의 모든 커넥션은 DB와 TCP/IP로 연결되어 있어 별다른 처리가 필요하지 않다.

  • 이러한 커넥션 풀은 스프링부트에서 HikariCP로 기본설정되어 제공한다.
    널리 사용되는 커넥션 풀 라이브러리 중 하나로 대량의 동시 연결 요청에 대해 최적화되어 있다.

  • 사용하는 이유

    • 연결마다 Connection 객체를 생성하고 소멸시키는 비용을 줄일 수 있다.
    • 미리 생성된 Connection 객체를 사용하기 때문에, DB 접근 시간이 단축된다.
    • DB에 접근하는 Connection의 수를 제한하여, 메모리와 DB에 걸리는 부하를 조정할 수 있다.
  1. 자바 설정
HikariConfig hikariConfig = new HikariConfig();
   
   hikariConfig.setDriverClassName("com.mysql.cj.jdbc.Driver");
   hikariConfig.setJdbcUrl("jdbc:mysql://127.0.0.1:3306/스키마이름");
   hikariConfig.setUsername("");
   hikariConfig.setPassword("");
   hikariConfig.addDataSourceProperty("cachePrepStmts", "true");
   hikariConfig.addDataSourceProperty("prepStmtCacheSize", "250");
   hikariConfig.addDataSourceProperty("prepStmtCacheSqlLimit", "2048");

   HikariDataSource dataSource = new HikariDataSource(hikariConfig);
   Connection connection = dataSource.getConnection();
    // 최초로 커넥션을 얻어오는 시점에 커넥션 풀이 채워진다
  1. xml 설정
<bean id="hikariConfig" class="com.zaxxer.hikari.HikariConfig">
        <property name="driverClassName" value="org.mariadb.jdbc.Driver"></property>
        <property name="jdbcUrl" value="jdbc:mariadb://localhost:3306/webdb"></property>
        <property name="username" value="webuser"></property>
        <property name="password" value="webuser"></property>
        <property name="dataSourceProperties">
            <props>
                <prop key="cachePrepStmts">true</prop>
                <prop key="prepStmtCacheSize">250</prop>
                <prop key="prepStmtCacheSqlLimit">2048</prop>
            </props>
        </property>
    </bean>

    <bean id="dataSource" class="com.zaxxer.hikari.HikariDataSource"
          destroy-method="close">
        <constructor-arg ref="hikariConfig" />
    </bean>

데이터베이스의 URL, 아이디, 비밀번호를 설정하고 커넥션을 가지고 올 때, 커넥션 풀에 최대 사이즈만큼 다른 쓰레드를 이용해서 커넥션이 채워지게 된다.

3. transaction

  • 데이터베이스에서 수행되는 하나의 논리적인 작업 단위를 말합니다. 트랜잭션은 데이터베이스의 상태를 일관된 상태로 유지하고 데이터의 무결성을 보장하는 데 사용됩니다.

3-1 commit, rollback

  1. commit : 모든 작업이 성공해서 DB에 반영되는 것
    (1) 자동 커밋 : sql문장 하나를 실행할 때마다 자동으로 commit 실행, DB의 기본 설정임
    (2) 수동 커밋 : 수동 커밋모드로 설정해야 트랜잭션 기능을 제대로 사용할 수 있다. 마지막에 commit; 를 해야 커밋이 실행된다. 수동으로 커밋을 하기 전까지는 다른 세션에서는 변경된 데이터를 볼 수 없다.
    set auto commit false
set autocommit false;
update member set money=10000 - 2000 where member_id = 'memberA';
update member set money=10000 + 2000 where member_id = 'memberB';
commit;  // 마지막에 커밋을 해주면 A에서 2000원이 감소하고 B에서 2000원이 감소
한다. 이렇게 작업단위가 하나로 묶이는 것이다.

set autocommit false;
update member set money=10000 - 2000 where member_id = 'memberA'; 
//성공해서 A의 돈 2000원 감소
update member set money=10000 + 2000 where member_iddd = 'memberB'; 
//쿼리 예외발생으로 실행되지 않음, B의 돈은 10000원 그대로
commit;

개발자가 sql문을 잘못 작성해서 A의 돈만 줄어드는 상황이 발생했고 commit를 했다면 A의 돈만 감소하고 B의 돈은 그대로인 치명적인 문제가 발생하게된다. commit를 하기 전에 이러한 문제가 발생했음을 인지했다면 rollback을 해야한다.

  1. rollback : 작업이 하나라도 실패해서 작업 이전상태로 되돌리는 것

3-2 transaction ACID

  1. 원자성 : 여러개의 작업을 하나로 묶었을 때, 하나의 작업처럼 성공하거나 실패 즉, 한 번에 커밋하거나 한 번에 롤백한다.
  2. 일관성 : 모든 트랜잭션은 일관성있는 DB 상태를 유지해야한다
  3. 격리성 : 동시에 실행되는 트랜잭션들이 서로에게 영향을 주지 않는다.
    (격리 수준이 레벨로 나누어져 있는데 나중에 찾아본다)
  4. 지속성 : 트랜잭션이 성공적으로 종료되면 결과가 기록되어야 한다.

3-3 transaction 적용

  1. 트랜잭션은 비즈니스 로직이 있는 서비스계층에서 사용한다. 원자성에 따라 한 번에 커밋 또는 롤백해야 하기 때문이다.

  2. 트랜잭션을 시작하려면 커넥션이 필요하다. 따라서 서비스 계층에서 커넥션을 만들어야하고 트랜잭션이 커밋 또는 롤백으로 종료된 후에 커넥션을 종료해야 한다.
    커넥션 생성 -> 트랜잭션 시작 -> 커밋,롤백 -> 트랜잭션 종료 -> 커넥션 종료

  3. 같은 세션을 사용하기 위해 트랜잭션을 사용하는동안 같은 커넥션을 유지해야한다. 따라서 커넥션을 끊는 것은 물론이고 새로 getConnection()으로 새로운 커넥션을 가지고 오는 것도 안된다.

JDBC
Connection con = dataSource.getConnection();
con.setAutoCommit(false);//트랜잭션 시작

// 비즈니스 로직 수행
 memberRepository.update(con, fromId, fromMember.getMoney() - money); 
// 파라미터로 같은 커넥션을 넘겨준다
 
con.commit(); , con.rollback //성공시 커밋 , 실패시 롤백
con.setAutoCommit(true);  // 기본값으로 세팅하고 닫기
con.close()
  • 문제 : 트랜잭션은 비즈니스 로직이 있는 서비스 계층에 있는 것이 맞지만 가급적 의존성 없어야하는 서비스계층이 DataSource, Connection 등JDBC에 의존성이 생긴다는 것. 이렇게 되면 나중에 JDBC 기술을 다른 기술로 바꾸게 될 때, 모든 서비스계층을 수정해야 한다.

3-3-1 트랜잭션 추상화, 동기화

  • 트랜잭션 추상화 : 스프링에서는 다양한 기술들의 트랜잭션 시작,종료방법을 이미 인터페이스로 구현해놓았다. 트랜잭션 매니저라고 부른다.

  • 트랜잭션 동기화 : 앞에서 같은 세션을 사용하기 위해 트랜잭션을 사용하는동안 같은 커넥션을 유지해야한다고 말했다. 이를 위해 파라미터로 커넥션을 넘겼는데 스프링에서 같은 커넥션을 유지하기 위한 기능을 제공한다. 이것을 트랜잭션 동기화 매니저라고 부른다.

  • 트랜잭션 매니저, 동기화 매니저를 사용하면서 커넥션을 파라미터로 넘기지 않는 매서드와 넘기는 매서드 둘 중에 넘기는 매서드가 필요없어진다.

  1. 트랜잭션 매니저가 커넥션을 생성하고 트랜잭션을 시작한다. 그리고 해당하는 커넥션을 트랜잭션 동기화 매니저에게 보내서 보관한다.
트랜잭션 동기화를 사용하려면 DataSourceUtils를 사용해야한다.
트랜잭션 동기화 매니저가 관리하는 커넥션이 있으면 해당 커넥션을 반환하고, 없으면
커넥션을 새로 만들어서 반환한다.
Connection con = DataSourceUtils.getConnection(dataSource);
  1. repository는 동기화 매니저에 저장된 커넥션을 가져가 사용한다.

  2. 커밋 또는 롤백으로 트랜잭션을 종료하면 트랜잭션 매니저가 동기화 매니저가 보관한 커넥션을 가져와 커넥션을 닫는다.

트랜잭션 동기화 매니저가 관리하는 커넥션인 경우 커넥션을 닫지 않고 그 외에는 닫는다.
DataSourceUtils.releaseConnection(con, dataSource);
  1. 서비스계층에서는 매니저를 사용하기 위해서 위에서 말한 PlatformTransactionManager 를 사용한다
private final PlatformTransactionManager transactionManager;

TransactionStatus status = transactionManager.getTransaction(new
DefaultTransactionDefinition());

transactionManager.commit(status);
transactionManager.rollback(status);

=> 커넥션을 파라미터로 전달할 필요가 없다.

3-4 transaction 과정

  1. 클라이언트가 서비스를 요청해서 비즈니스 로직를 실행

  2. 서비스 계층에서 트랜잭션 매니저를 통해 트랜잭션 시작을 요청

  3. 트랜잭션을 시작하기 위해서 DB 커넥션이 필요한데 매니저는 datasource를 사용해서 커넥션을 생성

  4. 커넥션을 수동 커밋모드로 변경

  5. 생성된 커넥션은 동기화 매니저에 보관

  6. 서비스 계층에서 비즈니스 로직을 실행하면서 repository의 메서드를 호출

  7. repository의 메서드들은 트랜잭션이 시작된 커넥션을 동기화 매니저에서 가지고 옴

  8. 획득한 커넥션을 사용해서 repository에서 SQL을 DB에 전달하고 데이터를 가져옴

  9. 비즈니스 로직이 완료되어 커넥션을 종료하려면 동기화 매니저에 맡겨놓은 커넥션을 가지고 와야한다. 그걸 가져와서 트랜잭션을 커밋 또는 롤백하고 커넥션을 닫는다
    데이터소스 - 트랜잭션 매니저 - repository - service

3-4-1 TransactionTemplate

  • 비즈니스 로직을 제외하고 트랜잭션을 시작하고 종료하는 모든 과정은 중복된다. 이를 위해 템플릿이 존재한다
  1. execute() : 반환 값이 있을 때 사용
  2. executeWithoutResult() : 반환 값이 없을 때 사용
private final TransactionTemplate txTemplate;
// TransactionTemplate 을 사용하려면 transactionManager 가 필요하다

public MemberService(PlatformTransactionManager transactionManager,
MemberRepository memberRepository) {
 	this.txTemplate = new TransactionTemplate(transactionManager);
 	this.memberRepository = memberRepository;
 }
 
 
 public class MemberRepositoryV5 implements MemberRepository {

    private final JdbcTemplate template;

    public MemberRepositoryV5(DataSource dataSource) {
        this.template = new JdbcTemplate(dataSource);
    }

 
 트랜잭션 템플릿을 사용할 때는 람다식으로..
 txTemplate.executeWithoutResult((status) -> { 비즈니스 로직 }

TransactionTemplate을 포함한 지금까지의 과정 모두 큰 문제점이 하나 있는데 비즈니스 로직에 트랜잭션 과정이 들어가 있어 서비스 계층이 순수하지 못하다는 것이다.

3-4-2 Transaction AOP

  • Transaction AOP를 사용하면 트랜잭션을 처리하는 로직만 따로 분리하여 Transaction Proxy를 만들 수 있다. @Transactional 메서드 또는 클래스에 사용하면 자동으로 Transaction Proxy 생성해준다. 그리고 @Transactional이 적용되려면 스프링 빈이어야 한다.

    => 앞에서 본 것처럼 코드로 트랜잭션을 다루는 방법은 거의 사용하지 않고 @Transactional을 사용한다. 그냥 작동원리, 구조만 파악하는 정도로 마무리하자.

3-4-3 스프링부트

@Bean
DataSource dataSource() {
 return new DriverManagerDataSource(URL, USERNAME, PASSWORD);
}
@Bean
PlatformTransactionManager transactionManager() {
 return new DataSourceTransactionManager(dataSource());
}
  • 원래는 위처럼 dataSource와 트랜잭션 매니저를 직접 bean으로 등록해야했지만 스프링부트가 이런 것들을 모두 자동으로 처리한다.
    dataSource는 application.properties에 입력된 url,username,password로 생성되고, 트랜잭션 매니저는 라이브러리를 보고 자동으로 맞는 것을 생성한다.

4. DB 구조와 세션

  • 세션이란 클라이언트와 서버 간의 상태를 유지하기 위한 메커니즘으로 클라이언트와 서버의 요청과 응답 간에 유지되어야 하는 데이터를 저장하고 관리하는 데 사용됩니다.
  1. 데이터베이스 서버에 접근하여 연결을 요청하고 커넥션을 생성
  2. DB는 내부에 세션을 만들고 커넥션을 통해 들어오는 요청들을 세션을 사용해서 실행
  3. 세션이 트랜잭션을 시작하고 커밋, 롤백을 통해 트랜잭션을 종료
  4. 커넥션을 닫거나 세션을 강제종료하면 세션 종료

4-1 DB lock

  1. 세션이 트랜잭션을 시작하면 그 세션에서 커밋이나 롤백을 하기 전까지는 다른 세션에서 데이터를 수정할 수 없게 한다
  2. lock를 가지고 있는 세션에서 데이터를 수정할 수 있고, 없는 세션은 lock이 돌아올 때까지 대기하다가 일정한 대기시간이 넘어가면 타임아웃 오류가 발생한다. (SET LOCK_TIMEOUT)
  3. 데이터를 수정할 때 락을 가져가는 것뿐만아니라 조회하는 시점에도 락을 가져갈 수 있다. ex) 마감 처리 select for update
select * from member where member_id='memberA' for update

5. 예외처리

  • 예외처리를 고민할 때는 시스템 예외인지, 비즈니스 예외인지 생각해 보아야 한다.
  1. 체크 예외 : 컴파일러가 체크하는 예외 , 반드시 throws로 던지거나 try-catch로 처리해야한다. 체크예외의 경우 모든 예외를 던지거나 처리해야하므로 귀찮아질 수 있고 해당 계층에서 처리할 수 없는 예외까지 다루어야한다는 단점이 있다.

  2. 언체크 예외 : 컴파일러가 체크하지 않는 예외로 예외를 잡지않아도 throws를 생략할 수 있다. 예외를 잡지않으면 자동으로 throws 한다

5-1 check 예외 활용

  • 대부분은 런타임 예외로 처리하고 비즈니스 로직상 발생한 예외나 매우 중요한 문제는 체크 예외를 사용한다.

    체크예외로 처리하는 것이 확실한데 사용하지 않는 이유는 발생하는 대부분의 예외들은 복구가 불가능한 시스템 예외들이며 위의 그림처럼
    자신이 처리할 수 없는 예외를 던지다보면 해당하는 예외를 알 필요가 없는 계층까지도 던져진 예외를 받게되기 때문이다. 이렇게되면 해당 계층에 의존성이 생기게된다. 따라서 나중에 기술을 변경하면 그 기술에 의존성이 있는 모든 코드를 수정해야한다.

5-2 uncheck 예외 활용

  • uncheck 예외 활용를 사용하면 controller나 service에서 예외에 의존하지 않아도 된다. 또한 기술을 변경할 시 예외를 공통으로 처리하는 부분만 코드를 수정하면 된다. 시스템문제로 발생한 예외인 경우에 보통 unchecked예외 처리를 한다.
  1. 런타임예외를 상속받는 클래스를 생성
  2. 체크예외가 발생하는 부분에서 런타임예외를 생성한다.

 static class RunRepository extends RuntimeException{
        public RunRepository(String message) {
            super(message);
        }
    }
    
static class Repository extends RuntimeException{
        public void call(){
            try {
                sql();
            } catch (SQLException e) {
                throw new RunRepository("message");
            }
        }

        public void sql() throws SQLException {
            throw new SQLException();
        }
    }

5-3 스택 트레이스

  • 위처럼 예외를 다른 예외로 변환할 때꼭 기존예외를 포함해야한다. 그렇게 하지 않으면 예외가 발생한 지점을 알 수 없다.
public void call() {
 try {
 	runSQL();
 } catch (SQLException e) {
 	throw new RuntimeSQLException(e); 
    //기존 예외인 SQLException의 e를 가지고 있어야한다
 }
  • 로그를 출력할 때 마지막 파라미터에 예외를 넣어주면 로그에 스택 트레이스를 출력할 수 있다. e.printStackTrace() 는 System.out을 사용하는 것으로 실무에서는 사용하지 않는다.
try {
 	controller.request();
 } catch (Exception e) {
   //e.printStackTrace();
   log.info("ex", e);
 }

6. 예외처리

6-1 인터페이스

  • 인터페이스의 구현체인 클래스가 체크예외를 사용하려면 인테페이스에도 체크예외가 선언되어 있어야 한다. 이렇게 되면 인터페이스 역시 특정 기술에 종속되어 인터페이스를 만든 의미가 없어진다.
  1. RuntimeException을 상속받는 Exception클래스를 생성
  2. 체크예외가 발생하는 부분에서 예외를 throw할 때, RuntimeException을 상속받은 클래스를 던짐, 이러면 모두 런타임 예외로 처리되어 인터페이스도 체크예외로 처리할 필요가 없음
public class MyDbException extends RuntimeException{ 
// 런타임 예외를 상속받으면서 런타입오류 클래스가 됨
    public MyDbException() {
    }

    public MyDbException(String message) {
        super(message);
    }

    public MyDbException(String message, Throwable cause) {
        super(message, cause);
    }

    public MyDbException(Throwable cause) {
        super(cause);
    }
}

catch (SQLException e) {
            throw new MyDbException(e);

6-2 예외 변환

  • 하지만 위처럼 처리하면 모든 예외를 묶어 런타임예외로 처리하므로 어떤 예외가 발생했는지 구분할 수 없다는 단점이 있다.

    DB에 요청이 들어오고 예외가 발생하면 어떤 예외인지 알 수 있는 오류코드를 Exception과 함께 반환한다. 서비스 계층에서는 여기에 담긴 오류코드를 확인하려면 또 특정기술에 의존성을 가지게 되므로 다른 예외클래스를 생성한다.
public class MyDuplicationEx extends MyDbException{


    public MyDuplicationEx() {
    }

    public MyDuplicationEx(String message) {
        super(message);
    }

    public MyDuplicationEx(String message, Throwable cause) {
        super(message, cause);
    }

    public MyDuplicationEx(Throwable cause) {
        super(cause);
    }
}

이전에 생성했던 RuntimeException을 상속받은, MyDbException을 상속받으면 의미있는 예외클래스를 만들 수 있다.

catch (SQLException e) {
 //h2 db
   if (e.getErrorCode() == 23505) {
   	throw new MyDuplicateKeyException(e);
   	}  

이렇게 직접 만든 예외로 처리를 하면 특정 기술에 의존하지 않고 서비스 계층에서 예외를 처리할 수 있지만 직접 예외코드를 찾아보고 예외를 만드는 것은 굉장히 힘들다
그런데 스프링에서는 데이터베이스에서 발생한 오류코드를 스프링에서 정의한 예외로 자동변환해주는 기능을 제공한다. 각 예외들은 기술에 종속적이지 않게 만들어졌다. 따라서 우리가 직접 예외를 새로 만들 필요가 없다.

 private final SQLErrorCodeSQLExceptionTranslator exTranslator;

    public MemberRepositoryV4_2(DataSource dataSource) {
        this.dataSource = dataSource;
    this.exTranslator = new SQLErrorCodeSQLExceptionTranslator(dataSource);
    }
    
     catch (SQLException e) {
            throw exceptionTranslator.translate("save", sql, e);

변수로 예외변환기를 선언하고 생성자로 datasource를 주입해서 사용한다.

6-3 에러와 예외

  • 에러

    • 에러(Error)는 일반적으로 심각한 문제로서 프로그램이 복구할 수 없는 상태에 도달한 경우를 나타냅니다. 이러한 상황에서는 보통 프로그램이 중단되거나 종료됩니다.
      에러는 주로 시스템 레벨에서 발생하며, 예를 들어 메모리 부족, 스택 오버플로우, 무한 루프 등이 있습니다.
      에러는 일반적으로 프로그래머가 직접 처리하지 않고, 시스템 레벨에서 처리되거나 로깅되어 관리자에게 보고될 수 있습니다.
  • 예외

    • 예외는 프로그램이 잘 동작하면서도 처리할 수 있는 문제를 의미합니다.
      예외는 런타임(Runtime) 중에 발생하며, 프로그래머가 예외 처리(Exception Handling)를 통해 이를 대응할 수 있습니다.
      예외는 종종 프로그래머의 코드에 의해 발생할 수 있는 잘못된 입력, 잘못된 조건, 파일을 찾을 수 없음과 같은 일상적인 문제를 나타내며, 예외 처리를 통해 프로그램의 안정성을 높일 수 있습니다.

0개의 댓글