[TIL] Ioc, Bean Scope, JPA

이진일·2026년 4월 20일

Spring에서는 왜 new를 쓰지 않는가?

자바를 처음 배우며 객체를 생성하는 방법으로 new를 사용한다고 배웠었다.

User user = new User();

하지만 스프링 프레임워크를 사용하여 개발을 할 때는 new를 거의 사용하지 않는다.

MemberRepository repository = new MemberRepository();
MemberService service = new MemberService(repository);
MemberController controller = new MemberController(service);

이런 식으로 Controller, Service, Repository 객체를 직접 생성하는 코드를 작성하지 않았다. 우리가 스프링 프레임워크를 사용하면 스프링이 객체를 생성하고 관리한다.

IoC

이것이 IoC(Inversion of Control)이다. 원래는 직접 객체를 생성하고 관리하여 제어하였는데, 이 제어권이 스프링에게 넘어가 제어의 역전이 일어난 것이다.

DI - 필요한 것을 넣어 주는 방식

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 생략 가능

ApplicationContext - Spring의 객체 창고

Spring에서 객체를 관리하는 컨테이너를 ApplicationContext라고 부른다. 그리고 이 컨테이너가 관리하는 객체를 빈(Bean)이라고 한다.

Spring이 애플리케이션이 시작될 때 @Component 애너테이션 붙은 클래스들을 ApplicationContext에 등록하여 빈으로 관리한다.

객체는 몇 개 만들어지는가

빈 스코프란?

Spring이 빈을 만들 때, "이 빈을 몇 개 만들 것인가"와 "얼마나 오래 살려 둘 것인가"를 정하는 규칙이 있다. 이것을 빈 스코프(Bean Scope)라고 부른다.

스코프의미언제 쓰나
singleton컨테이너에 하나대부분의 Service, Repository
prototype요청할 때마다 새 객체상태를 분리해야 할 때
requestHTTP 요청마다 하나요청별 상태
session세션마다 하나로그인 사용자 상태
application애플리케이션 전체 공유전역 공유 자원

스프링에서는 기본값이 singleton이다.

왜 singleton인가?

대부분의 Service와 Repository는 상태를 저장하지 않는(stateless) 객체이기 때문에 객체를 하나만 만드는 singleton을 사용한다.

sigleton에서 주의할 점⚠️

singleton을 쓸 때는 필드에 사용자별 상태를 저장하면 안된다. 하나의 객체를 여러 요청이 동시에 사용하기 때문에 상태가 저장되어 있으면 섞일 수 있다.

JPA — SQL 없이 DB와 대화하기

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();
  • SQL을 직접 작성하고 단순 반복되는 코드를 작성해주어야 했다.

Hibernate의 두둥등장

이런 불편함을 해결하기 위해 등장한 것이 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는 창고이고, 영속성 컨텍스트는 창고 앞에 놓인 작업대다. 재료를 창고에서 꺼내서 작업대에 올려놓고, 작업이 끝나면 다시 창고에 넣는다. 영속성 컨텍스트가 이 작업대 역할을 한다.

영속성 컨텍스트의 네 가지 기능

  • 1차 캐시 : 같은 트랜잭션 안에서 같은 Entity를 여러번 조회하면 두 번째부터는 DB에 직접 조회하지 않고 캐시에서 바로 꺼내간다.
  • 동일성 보장: 같은 식별자로 조회한 Entity는 항상 같은 인스턴스를 반환한다.
  • 쓰기 지연: JPA는 SQL을 모아 두었다가 트랜잭션이 커밋될 때 한꺼번에 실행한다.
  • 변경 감지: Entity의 값을 수정할 때 Repository를 통해 별다른 메서드를 실행하지 않아도 객체의 값이 변경된 것을 감지하여 Update 쿼리를 실행한다.

엔티티 생명주기

상태의미예시
비영속 (Transient)객체만 만들어진 상태new User()
영속 (Persistent)영속성 컨텍스트에 관리되는 상태repository.save(user) 후
준영속 (Detached)영속성 컨텍스트에서 분리된 상태트랜잭션 종료 후
삭제 (Removed)삭제가 예약된 상태repository.delete(user) 후

@Transactional

@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 주의할 점⚠️


위 다이어그램이 @Transactional의 프록시 패턴이다. 외부에서 호출하면 Proxy를 거쳐서 TX가 적용되지만, 같은 클래스 내부에서 this로 호출하면 Proxy를 우회해서 TX가 적용되지 않는다. 이것이 @Transactional 애노테이션의 가장 유명한 함정이다.

private 메서드에는 적용되지 않는다.

위에서 @Transactional은 public 메서드에 붙여서 사용한다고 했다. 그 이유가 바로 프록시는 외부에서 호출할 수 있는 public 메서드만 감쌀 수 있기 때문이다.

같은 클래스 내부에서 this로 호출하면 적용되지 않는다.

this.saveOrder()는 프록시를 거치지 않고 실제 객체를 직접 호출한다. 프록시를 거치지 않으니 트랜잭션이 적용되지 않는다.

0개의 댓글