자바를 처음 배우며 객체를 생성하는 방법으로 new를 사용한다고 배웠었다.
User user = new User();
하지만 스프링 프레임워크를 사용하여 개발을 할 때는 new를 거의 사용하지 않는다.
MemberRepository repository = new MemberRepository();
MemberService service = new MemberService(repository);
MemberController controller = new MemberController(service);
이런 식으로 Controller, Service, Repository 객체를 직접 생성하는 코드를 작성하지 않았다. 우리가 스프링 프레임워크를 사용하면 스프링이 객체를 생성하고 관리한다.
이것이 IoC(Inversion of Control)이다. 원래는 직접 객체를 생성하고 관리하여 제어하였는데, 이 제어권이 스프링에게 넘어가 제어의 역전이 일어난 것이다.
DI는 IoC 원칙을 실현하는 구체적인 방법이다. DI(Dependency Injection)는 말그대로 의존성을 주입해 주는 것이다. Service가 Repository에 의존하니 Service에 Repository를 주입해준다.
의존성을 주입하는 방식은 크게 세가지가 있다.
@Service
public class MemberService {
@Autowired
private MemberRepository memberRepository;
}
필드에 명시하여 @Autowired 애너테이션을 붙여 의존성을 주입한다.
@Service
public class MemberService {
private MemberRepository memberRepository;
@Autowired
public void setMemberRepository(MemberRepository repo) {
this.memberRepository = repo;
}
}
setter메서드에 @Autowired 애너테이션을 붙여 의존성을 주입한다.
@Service
public class MemberService {
private final MemberRepository memberRepository;
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
}
생성자를 통해 의존성을 주입해준다. 생성자가 하나라면 @Autowired 생략 가능
Spring에서 객체를 관리하는 컨테이너를 ApplicationContext라고 부른다. 그리고 이 컨테이너가 관리하는 객체를 빈(Bean)이라고 한다.
Spring이 애플리케이션이 시작될 때 @Component 애너테이션 붙은 클래스들을 ApplicationContext에 등록하여 빈으로 관리한다.
Spring이 빈을 만들 때, "이 빈을 몇 개 만들 것인가"와 "얼마나 오래 살려 둘 것인가"를 정하는 규칙이 있다. 이것을 빈 스코프(Bean Scope)라고 부른다.
| 스코프 | 의미 | 언제 쓰나 |
|---|---|---|
| singleton | 컨테이너에 하나 | 대부분의 Service, Repository |
| prototype | 요청할 때마다 새 객체 | 상태를 분리해야 할 때 |
| request | HTTP 요청마다 하나 | 요청별 상태 |
| session | 세션마다 하나 | 로그인 사용자 상태 |
| application | 애플리케이션 전체 공유 | 전역 공유 자원 |
스프링에서는 기본값이 singleton이다.
대부분의 Service와 Repository는 상태를 저장하지 않는(stateless) 객체이기 때문에 객체를 하나만 만드는 singleton을 사용한다.
singleton을 쓸 때는 필드에 사용자별 상태를 저장하면 안된다. 하나의 객체를 여러 요청이 동시에 사용하기 때문에 상태가 저장되어 있으면 섞일 수 있다.
Spring Data JPA가 없던 시절에는 자바에서 DB와 대화하려면 이런 코드를 작성해야 했다.
String sql = "SELECT * FROM users WHERE id = ?";
Connection conn = DriverManager.getConnection(url, user, password);
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setLong(1, userId);
ResultSet rs = pstmt.executeQuery();
if (rs.next()) {
User user = new User();
user.setId(rs.getLong("id"));
user.setEmail(rs.getString("email"));
user.setName(rs.getString("name"));
}
rs.close();
pstmt.close();
conn.close();
이런 불편함을 해결하기 위해 등장한 것이 ORM이다. 이 ORM의 대표적인 구현체가 Hibernate이다.
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String email;
@Column(nullable = false)
private String name;
}
이렇게 클래스에 애노테이션을 붙여 두면 Hibernate가 이 클래스와 users 테이블을 자동으로 매핑한다. 그러면 저장할 때 SQL을 직접 쓸 필요 없이 이렇게만 하면 된다.
ORM이 등장하고 초기에는 여러 프레임워크가 나왔었는데 각기 사용법이 달랐기 때문에 프레임워크를 변경하기가 어려웠다.
그래서 JPA라는 표준이 탄생했다.
영속성 컨텍스트(Persistence Context)는 애플리케이션과 데이터베이스 사이에서 객체를 보관하는 가상의 공간이다.
식당 비유로 설명하면, DB는 창고이고, 영속성 컨텍스트는 창고 앞에 놓인 작업대다. 재료를 창고에서 꺼내서 작업대에 올려놓고, 작업이 끝나면 다시 창고에 넣는다. 영속성 컨텍스트가 이 작업대 역할을 한다.
| 상태 | 의미 | 예시 |
|---|---|---|
| 비영속 (Transient) | 객체만 만들어진 상태 | new User() |
| 영속 (Persistent) | 영속성 컨텍스트에 관리되는 상태 | repository.save(user) 후 |
| 준영속 (Detached) | 영속성 컨텍스트에서 분리된 상태 | 트랜잭션 종료 후 |
| 삭제 (Removed) | 삭제가 예약된 상태 | repository.delete(user) 후 |
@Transactional이 붙은 메서드는 트랜잭션 안에서 실행된다. 트랜잭션이란 "여러 개의 작업을 하나로 묶어서, 전부 성공하면 확정하고, 하나라도 실패하면 전부 되돌리는" 단위다.
@Service
@RequiredArgsConstructor
public class TransferService {
private final AccountRepository accountRepository;
@Transactional
public void transfer(Long fromId, Long toId, int amount) {
accountRepository.withdraw(fromId, amount); // A 계좌 출금
accountRepository.deposit(toId, amount); // B 계좌 입금
}
}
만약 @Transactional 없이 A 계좌에서 출금 후 서버가 죽으면 어떻게 될까?
A의 돈은 사라졌는데 B에는 도착하지 않은 상태가 된다. @Transactional이 있으면 예외 발생 시 출금도 롤백되어서 두 작업이 함께 취소된다.
@Transactional 애너테이션은 Service의 public 메서드에 붙이면 된다.

위 다이어그램이 @Transactional의 프록시 패턴이다. 외부에서 호출하면 Proxy를 거쳐서 TX가 적용되지만, 같은 클래스 내부에서 this로 호출하면 Proxy를 우회해서 TX가 적용되지 않는다. 이것이 @Transactional 애노테이션의 가장 유명한 함정이다.
위에서 @Transactional은 public 메서드에 붙여서 사용한다고 했다. 그 이유가 바로 프록시는 외부에서 호출할 수 있는 public 메서드만 감쌀 수 있기 때문이다.
this.saveOrder()는 프록시를 거치지 않고 실제 객체를 직접 호출한다. 프록시를 거치지 않으니 트랜잭션이 적용되지 않는다.