대부분의 엔터프라이즈 애플리케이션은 어떤 방법으로든 백엔드 시스템, 특히 데이터베이스와 연결돼서 동작한다. 애플리케이션이 다루는 데이터를 장기적으로 보존하고 효과적으로 검색할 수 있게 해주는 데이터베이스의 이용은 필수이다. 자바에는 JDBC라는 호환성이 뛰어나고 편리한 API를 가진 데이터 액세스 기술이 있다. 하지만 전통적인 SQL과 JDBC API조합만으로는 엔터프라이즈 애플리케이션의 복잡하고 거대한 데이터를 처리하는 깔끔한 코드를 만드는 데는 많은 제약과 문제가 있다. 자바에는 JDBC이외에도 다양한 데이터 엑세스 기술이 존재한다. 이러한 옵션들을 알아보겠다.
1. 공통 개념
1.1. DAO 패턴
데이터 액세스 계층은 DAO 패턴이라 불리는 방식으로 분리하는 것이 원칙이다. 비즈니스 로직이 없거나 단순하면 DAO 서비스 계층을 통합할 수도 있으나, 의미 있는 비즈니스 로직을 가진 엔터프라이즈 애플리케이션이라면 DAO 패턴으로 분리해야 한다.
DAO 패턴은 DTO또는 도메인 오브젝트만을 사용하는 인터페이스를 통해 데이터 엑세스 기술을 외부에 노출하지 않도록 하는것이다. 따라서 DAO는 구현 기술에 대한 정보를 외부에 공개해선 안된다. 가장 중요한 장점은 DAO를 이용하는 서비스 계층의 코드를 기술이나 환경에 종속되지 않는 순수한 POJO로 개발할 수 있다는 것이다. DAO 인터페이스는 기술과 상관없는 단순한 DTO나 도메인 모델만을 사용하기 때문에 언제든지 목 오브젝트같은 테스트 대역 오브젝트로 대체해서 단위 테스트를 작성할 수 있다.
1.1.1. DAO 인터페이스와 DI
DAO는 인터페이스를 이용해 접근하고 DI 되도록 만들어야 한다. DAO 인터페이스에는 구체적인 데이터 엑세스 기술과 관련된 어떤 API나 정보도 노출하지 않는다.
인터페이스를 만들 때 DAO 클래스의 모든 Public 메서드를 추가하지 않도록 주의하자. 의미있는 메서드만 인터페이스로 공개해야 한다.
1.1.2. 예외처리
데이터 엑세스 중에 발생하는 예외는 대부분 복구할 수 없다. 따라서 DAO 밖으로 던져질 때는 런타임 예외여야 한다. 또한, DAO 메서드 선언부에 throws SQLException과 같은 내부 기술을 드러내는 예외를 직접 노출해선 안된다. throws Exception과 같은 것도 안된다. 무조건 런타임 예외로 전환해야 한다. 서비스 계층 코드는 DAO가 던지는 대부분의 예외를 직접 다룰 필요가 없다.
최신 데이터 액세스 기술은 JDBC와는 다르게 런타임 예외를 사용한다. 기술에 독립적인 추상 예외로 전환하고 일관된 예외 복구 기능을 적용할 필요가 없다면 굳이 스프링의 예외 반환 서비스를 사용하지 않아도 된다.
1.2. 템플릿과 API
데이터 액세스 기술을 사용하는 코드 대부분은 try-catch-finally와 판에 박힌 반복되는 코드로 작성되기 쉽다. 데이터 엑세스 기술은 애플리케이션 외부의 리소스와 연동하기 때문에, 다양한 예외상황이 발생할 수 있다. 이런 예외상황에서 서버의 제한된 리소스에 누수가 발생하지 않도록 예외상황에서도 사용한 리소스를 적절히 반환해주는 코드가 반드시 필요하다. 이 때문에 코드가 길고 지저분해지기 쉽다.
스프링의 템플릿/콜백 패턴을 이용해 이런 판에 박힌 코드를 피하고 꼭 필요한 바뀌는 내용만 담을 수 있도록 템플릿을 제공한다. 미리 만들어진 작업 흐름은 반복 코드를 제거해줄 뿐만 아니라 예외 변환과 트랜잭션 동기화 기능도 함께 제공해준다.
템플릿의 단점은 템플릿이 제공하는 API를 사용해야 한다는 점이다. 또한, 콜백 오브젝트를 이해하기 어려울 수 있다. 그래서 스프링은 일부 데이터 엑세스 기술을 템플릿 대신, 해당 기술의 API 그대로 사용하게 해주기도 한다. 하지만 스프링이 지원하지 않는 방식으로 데이터 엑세스 기술을 사용하면 예외 변환과 트랜잭션 동기화 혜택을 받기 어렵기 때문에 피해야한다.
1.3. DataSource
JDBC를 통해 DB를 사용하려면 Connection타입의 DB 연결 오브젝트가 필요하다. Connection은 모든 데이터 엑세스 기술에서 사용되는 필수 리소스이다. 사용자 요청이 빈번하게 일어나는 엔터프라이즈 환경에선 각 요청마다 Connection을 새롭게 만들고 종료시킨다. 요청마다 서버와 새로 만들면 비효율적이고 성능을 떨어뜨린다. 따라서 정해진 갯수만큼 DB 커넥션을 pool에 미리 준비해두고, 애플리케이션이 요청할 때마다 풀에서 꺼내 하나씩 할당해주고 다시 돌려받아서 풀에 넣는 식의 풀링 기법을 활용한다.
스프링은 DataSource를 하나의 독립된 빈으로 등록하도록 강력하게 권장한다.
2. JDBC
JDBC는 자바의 데이터 엑세스 기술의 기본이 되는 로우레벨 API이다. JDBC는 표준 인터페이스를 제공하고 각 DB 벤더와 개발팀에서 이 인터페이스를 구현한 드라이버를 제공하는 방식으로 사용된다.
2.1. 스프링 JDBC 역할
Connection 열기와 닫기
스프링 JDBC를 사용하면 코드에서 직접 Connection을 열고 닫는 작업을 할 필요가 없다. Connection과 관련된 모든 작업은 스프링 JDBC가 필요한 시점에서 알아서 진행해준다. 진행 중에 예외가 발생했을 때도 문제없이 열린 모드 Connection 오브젝트를 닫아준다.
Statement 준비와 닫기
SQL 정보가 담긴 Statement또는 PreparedStatement를 생성하고 필요한 준비 작업을 해주는 것도 대부분 스프링 JDBC의 몫이다. 하지만 스프링 JDBC가 Statement를 준비하는 동안에 필요한 정보, 예를 들어 파라미터 바인딩에 사용할 정보가 담긴 맵이나 오브젝트를 미리 준비해주는 건 개발자의 책임이다.
Statement 실행
SQL이 담긴 Statement를 실행하는 것도 스프링 JDBC의 몫이다. Statement의 실행 결과는 다양한 형태로 가져올 수 있다.
ResultSet 루프
ResultSet의 루프를 만들어 반복해주는 것도 스프링 JDBC가 해주는 작업이다. ResultSet의 각 내용을 어떻게 오브젝트에 담을 것인지는 루프 안에서 실행되는 콜백으로 만들어 템플릿에 제공해주면 된다.
예외처리와 변환
JDBC 작업 중 발생하는 모든 예외는 스프링 JDBC의 예외 변환기가 처리해준다.
트랜잭션 처리
JDBC는 트랜잭션 동기화 기법을 이용해 선언적 트랜잭션 기능과 맞물려서 돌아간다. 트랜잭션을 시작한 후에 스프링 JDBC의 작업을 요청하면 진행 중인 트랜잭션에 참여한다.
3. iBatis SqlMaps
자바 오브젝트와 SQL문 사이의 자동매핑 기능을 지원하는 ORM 프레임워크다. iBatis는 코드 내에서 자바오브젝트만을 이용해 데이터 로직을 작성할 수 있게 해주고, SQL을 별도의 파일로 분리해서 관리하게 해주며, 오브젝트-SQL 사이의 파라미터 매핑 작업을 자동으로 해주기 때문에 많은 인기를 얻고 있는 기술이다.
가장 큰 특징은 SQL을 자바 코드에서 분리해서 별도의 XML파일 안에 작성하고 관리할 수 있다는 점이다. 따라서 SQL에 변경이 있을 때마다 자바 코드를 수정하고 컴파일하지 않아도 된다.
4. JPA
JPA는 Java Persistent API의 약자로 EJB 3.0과 함께 등장한 JavaEE와 JavaSE를 위한 영속성 관리와 O/R 매핑을 위한 표준 기술이다. 관계형 DB는 그 자체로 성능이 매우 뛰어나고 기능이 풍성한 기술임에 분명하지만, 오브젝트를 중심으로 개발하는 자바같은 언어를 통해 접근하려면 불편한 점이 많다.
ORM(Object-Relational Mapping, 객체 지향 프로그래밍 언어와 관계형 데이터베이스 간의 데이터를 자동으로 매핑(연결)해주는 기술)이란 JAVA의 오브젝트-RDB사이에 존재하는 개념과 접근 방법, 성격의 차이때문에 요구되는 불편한 작업을 제거해준다. 따라서 ORM을 사용하는 개발자는 모든 데이터를 오브젝트 관점에서 볼 수 있다. RDB에 담긴 정보를 자바오브젝트를 다루는 것만으로 관리할 수 있다는 뜻이다.
이전에는 하이버네이트, TopLink 등 대비 JPA가 큰 인기를 끌지 못했지만, ORM 전문가들이 EJB 3.0 의 스펙 작업에 참여하면서 기존 엔티티빈을 JPA라는 이름으로 바꾸고 표준 자바 영속성 관리와 ORM 기능을 제공하는 범용 ORM 기술로 발전시켰다. JPA는 JavaEE 뿐만 아니라 JavaSE에서도 사용이 가능하다. 또한 표준 기술이기 때문에 이를 준수하는 다양한 상용/오픈소스 제품이 등장할 수 있었고, 환경에 독립적인 ORM 프로그래밍이 가능해졌다.
4.1. EntityManager와 JpaTemplate
JDBC나 iBatis를 사용할 때는 스프링의 템플릿/콜백 패턴으로 만들어진 템플릿을 사용하는 코드로 DAO를 만들었다. 템플릿을 이용해 반복되는 코드를 제거하고 예외 변환가 트랜잭션 동기화 등의 기능을 적용할 수 있다. JPA도 마찬가지다. 그런데 스프링에서는 템플릿 방식의 JpaTemplate뿐 아니라 JPA API를 직접 사용해 DAO를 자성할 수도 있다.
JpaDaoSupport클래스를 상속해서 DAO를 만들면 JpaTemplate을 생성하는 코드는 생략할 수 있다. JpaTemplate가 필요하면 getJpaTempla() 메서드를 이용하면 된다.
5. 하이버네이트
가장 크게 성공한 오픈소스 ORM 프레임워크이다. 자바 언어의 장점을 포기하고 기술과 환경에 종속적인 복잡한 EJB 컴포넌트를 만들어야 했던 시절에, POJO 프로그램을 전면에 내세우면서 가볍고 단순한 코드로 객체지향 기술의 특징을 살려서 엔터프라이즈 시스템을 개발할 수 있음을 보여준 것이 스프링이다.
이와 거의 동시대에 등장해서, 복잡한 엔티티빈 대신 평범한 POJO로 SQL을 직접 사용하는 전통적인 방식 못지않게 강력하고 빠르면서도 편리한 ORM 방식의 개발이 가능함을 보여준게 하이버네트이다.
하이버네이트는 그 자체로 독립적인 ORM제품이면서 동시에 JPA의 핵심 구현 제품이기도 하다.
6. 트랜잭션
스프링이 처음 등장했을 때의 모토는 "객체지향의 원칙에 충실한 POJO에 엔터프라이즈 서비스를 제공한다"였다. 엔터프라이즈 서비스가 제공되는 컴포넌트를 지향하던 당시의 EJB는 특정 클래스의 상속과 인터페이스 구현을 강제하고 툴의 지원 없이는 작성이 거의 불가할 정도로 복잡한 XML설정과 고가의 서버와 컨테이너 등을 필요로 했다. EJB가 제공했던 엔터프라이즈 서비스에서 가장 매력적인 것은 바로 선언적 트랜잭션이다. 코드 내에서 직접 트랜잭션을 관리하고 트랜잭션 정보를 파라미터로 넘겨서 사용하지 않아도 되는, 트랜잭션 스크립트 방식의 코드를 탈피할 수 있다는 것이다.
트랜잭션 스크립트란 하나의 트랜잭션 안에서 동작해야 하는 코드를 한 군데 모아서 만드는 방식이다. 보통 트랜잭션마다 하나의 메서드로 구성된다. 메서드 앞부분부터 DB연결-DB엑세스-비즈니스 로직 적용 하는 코드가 뒤엉켜서 등장한다. 같은 트랜잭션 내에서 동작함을 보장하려면 Connection같은 트랜잭션 정보가 담긴 파라미터를 계속 물고 다녀야 한다. 트랜잭션 스크립트 방식의 코드에는 중복이 자주 발생한다.
하지만 선언적 트랜잭션 경계설정을 사용하면 이런 문제를 모두 해결할 수 있다. 트랜잭션이 시작되고 종료되는 지점은 별도의 설정을 통해 결정된다. 또한 작은 단위로 분리되어 있는 데이터 엑세스 로직과 비즈니스 로직 컴포넌트 메서드를 조합해서 하나의 트랜잭션에서 동작하게 만드는 것도 간단하다.
예를들어, A, B, C 메서드가 있다고 하자. 만일 A만 사용한다면 A에 대해 트랜잭션을 걸어주면 된다. 그러나 A-B, A-C, A-B-C 같은 구성으로 트랜잭션을 만들고 싶을 수도 있다. 이때 A-B 코드를 묶어서 하나의 새로운 트랜잭션 스크립트를 만들 필요가 없다. A에서 B의 코드를 호출하게 하고, 각각의 트랜잭션 전파 속성을 "트랜잭션 필요(REQUIRED)"로 해주면 된다.
EJB의 이런 선언적 트랜잭션 기능을 복잡한 환경이나 구현조건 없이 평범한 POJO로 만든 코드에 적용하게 해주는 것이 바로 스프링이다.
6.1. 트랜잭션 추상화와 동기화
트랜잭션 서비스의 종류는 매우 다양하다. 트랜잭션 서비스는 환경에 따라 달라질 수 있기 때문이다. 또한 스프링 없이 선언적 트랜잭션을 이용하려면 특정 기술과 서버 플랫폼, 특정 트랜잭션 서비스에 종속될 수밖에 없다. 스프링은 이러한 종속성을 제거해주고 추상 계층을 이용해서 트랜잭션 기능을 활용하도록 만들어준다.
동기화는 트랜잭션을 일정 범위 안에서 유지해주고, 어디서든 자유롭게 접근할 수 있게 만들어준다.
6.2. @Transactional
트랜잭션 AOP를 적용하는 방법 중 하나는 @Transactional 어노테이션을 사용하는 것이다. 이 방식은 트랜잭션이 적용될 타깃 인터페이스나 클래스, 메서드 등에 부여하여 트랜잭션 속성을 제공한다.
Transactional을 적용하는 우선순위는 높은 순서부터 클래스의 메서드, 클래스, 인터페이스 메서드, 인터페이스 순이다.
본격적인 엔터프라이즈 시스템이라면, 조회전용 메서드에 읽기전용 트랜잭션 속성@Transactional (readOnly=true)을 부여해서 성능을 최적화하는것이 기본이다.
6.3. AOP 방식 : 프록시와 AspectJ
스프링의 AOP는 기본적으로 프록시 방식이다. 스프링의 프록시 AOP대신 AspectJ의 AOP를 활용할 수 있다. AspectJ AOP는 스프링과 달리 프록시를 타깃 오브젝트 앞에 두지 않는다. 대신 타깃 오브젝트 자체를 조작해서 부가기능을 직접 넣는 방식이다. 마치 처음부터 타깃 오브젝트의 클래스에 부가기능을 가진 소스코드가 있었던 것 처럼 만들어준다.
6.4 트랜잭션 전파 : propagation
- REQUIRED
미리 시작된 트랜잭션이 있으면 참여하고, 없으면 새로 시작한다.
- SUPPORTS
이미 시작된 트랜잭션이 있으면 참여하고, 그렇지 않으면 트랜잭션 없이 진행한다.
- MANDATORY
미리 시작된 트랜잭션이 있으면 참여하고, 없으면 예외를 발생시킨다. 혼자서는 독립적으로 트랜잭션을 진행하면 안되는 경우에 사용한다.
- REQUIRES_NEW
항상 새로운 트랜잭션을 시작한다. 이미 진행중인 트랜잭션이 있으면 잠시 보류한다.
- NOT_SUPPORTED
트랜잭션을 사용하지 않게 된다. 이미 진행중인 트랜잭션이 있으면 보류시킨다.
- NEVER
트랜잭션을 사용하지 않도록 강제한다. 이미 진행중인 트랜잭션도 없어야 한다.
- NESTED
이미 진행중인 트랜잭션이 있으면 중첩 트랜잭션을 시작한다. 하지만 REQUIRES_NEW 와는 다르다.
중첩된 트랜잭션은 먼저 시작된 부모 트랜잭션의 커밋과 롤백에는 영향을 받지만, 자신의 커밋과 롤백은 부모에 영향을 주지 않는다. 예를들어, 작업을 진행하는 도중 작업 로그를 DB에 저장한다고 하자. 이때, 작업 로그 남기는것을 실패했다고 작업 전체를 되돌리는것은 손해다. 반면 작업을 실패한 경우 작업 로그도 제거해야 한다. 바로 이럴 때 로그 작업을 중첩 트랜잭션으로 만들어 두는 것.
6.5. 트랜잭션 격리 수준 : isolation
- DEFAULT
사용하는 데이터 엑세스 기술 또는 DB 드라이버의 디폴트 설정을 따른다. 대부분 DB는 READ_COMMITTED를 기본 격리수준으로 갖는다.
- READ_UNCOMMITTED
가장 낮은 격리수준이다. 하나의 트랜잭션이 커밋되기 전 그 변화가 다른 트랜잭션에 그대로 노출된다. 장점은 가장 빠르다는 것이다.
- READ_COMMITTED
가장 많이 사용되는 격리수준이다. 다른 트랜잭션이 커밋하지 않은 정보는 읽을 수 없다.
하지만 하나의 트랜잭션이 읽은 값을 다른 트랜잭션이 수정할 수 있다. 이 때문에 처음 트랜잭션이 같은 값을 다시 읽을 경우 다른 내용이 발견될 수 있다.
- REPEATABLE_READ
하나의 트랜잭션이 읽은 로우를 다른 트랜잭션이 수정하는 것을 막아준다. 하지만 새로운 로우를 추가하는 것은 제한하지 않는다. 따라서 SELECT로 전부 값을 가져오면 트랜잭션이 끝나기 전에 추가된 로우가 발견될 수 있다.
- SERIALIZABLE
가장 강한 트랜잭션 격리수준이다. 여러 트랜잭션이 동시에 같은 테이블의 정보를 엑세스하지 못한다. 가장 안전하지만 가장 성능이 떨어진다..따라서 안전한 작업이 필요한 경우가 아니라면 자주 사용하지 않는다.
'Backend > Spring' 카테고리의 다른 글
| JAVA의 Thread와 Vitrual Thread(in Spring Framework) (0) | 2025.11.29 |
|---|---|
| 8. 스프링이란 무엇인가? (0) | 2025.11.02 |
| AOP(Aspect Oriented Programming) (0) | 2025.09.24 |
| Spring에서의 테스트 (0) | 2025.09.16 |
| 자바와 스프링의 예외처리 (1) | 2025.09.10 |