- 객체와 테이블 매핑 : @Entity, @Table
- 필드와 컬럼 매핑 : @Column
- 기본키 매핑 : @Id, @GeneratedValue

 

 

객체와 테이블 매핑, 필드와 컬럼 매핑, 기본키 매핑의 방법을 간단하게 알아보고 각 애노테이션의 중요한 옵션들과 기능들에 대해서 알아보자.

 

 

 

 

객체와 테이블 매핑

 

 

 

@Entity가 붙은 클래스는 JPA가 관리하는 엔티티라고 한다.
- 기본 생성자가 필수이다.(파라메터가 없은 public또는 protected생성자) JPA기본 스펙이 이렇게 되어 있다.
- final 클래스, enum, interface, inner 클래스에 사용할수 없다.
- 저장할 필드에 final을 사용할 수 없다. 
- name 속성 : 같은 클래스 이름이 없으면 가급적이면 기본값을 사용하자.

- @Table : 아래와 같은 기능이 있는 속성들을 가진다.

 

 

 

 

 

 

데이터베이스 스키마 자동 생성

 

spring.jpa.hibernate.ddl-auto=create

- properties나 yml은 위과 같은 형식으로 옵션을 설정할 수 있다.

 

<property name="hibernate.hbm2ddl.auto" value="create" />

- xml은 위와 같은 형식으로 옵션을 설정할 수 있다.

 

 

 

- DDL을 애플리케이션 실행 시점에 자동 생성해줄 수 있다. 
- 테이블 중심의 개발에서 객체 중심의 개발을 할 수 있다.
- 데이터베이스 방언을 활용해서 데이터베이스에 맞는 적절한 DDL을 생성할 수 있다.

 

<property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"/>

- 데이터베이스 방언? : dialect를 사용하여 value에 원하는 데이터베이스를 설정하면 그에 맞게 하이버네이트에서 자동으로 쿼리를 변경해준다. 예를들어 MySQL에서 VARCHAR인 타입은 Oracle에서는 VARCHAR2인데, 이러한 차이를 자동으로 변경해준다.

- 실무에서는 사용하지 말고 개발단계에서만 사용하자. (테이블의 변경에서 문제가 생기면 큰 장애가 발생...)

 

 

 

 

 

 

 

필드와 컬럼 매핑

 

필드 매핑 애노테이션의 종류

 

@Column 애노테이션의 속성들

 

 

- @Temporal(TemperalType.DATE) 애노테이션은 Java8 이상부터 적용되는 LocalDate, LocalDateTime을 사용하게 되면, LocalDate는 Date타입으로(년,월,일) , LocalDateTime은 Timestamp타입으로 (년,월,일,시간) 인식된다.

 

- @Enumerated(EnumType.STRING) 애노테이션을 사용할 때, 옵션을 주의해야 한다. 디폴트옵션은 @Enumerated(EnumType.ORDINAL) 이다. 이는 Enum타입의 순서(index)를 사용한다. 이 방식에서의 문제점은 중간에 Enum타입에 값이 추가,변경 되었을 때 index값이 바뀐다는 것이다. 그러면 모든 데이터가 틀어지는 상황이 발생할 수도 있다. Enum타입을 사용할 때는 그냥 @Enumerated(EnumType.STRING)을 사용하자.

 

 

 

 

 

 

 

기본키 매핑

 

- @Id는 직접할당 방식으로 이 애노테이션을 사용하면 직접 PK값으로 등록한 것이다.

- @GeneratedValue는 자동할당 방식으로 전략에 따라 데이터베이스에 기본키 매핑방식을 위임한다.

- @Id나 @GeneratedValue로 등록하면 데이터베이스에 위임한 것이기 때문에 값을 등록하면 안된다. (코드에서 set하여 넣으면 안된다는 뜻)

 

 

@GeneratedValue의 전략

1. @GeneratedValue(strategy = GenerationType.IDENTITY)

기본 키 생성을 데이터베이스에 위임한다.
- Mysql을 사용하면 Auto Increase를 사용하여 만든다.

- 특징 : 이 전략을 사용하면 데이터베이스에 기본키 생성을 위임하기 때문에 영속성 컨텍스트에 영속(em.persist();)할 때, 기본키 값을 모른다! 1차 캐시에 담을 때 기본키를 key값으로 영속성 컨텍스트에 등록하기 때문에 발생하는 문제이다.  그래서 이 전략을 사용할 때에만 예외적으로 em.persist(); 하는 시점에 DB에 insert 쿼리가 날아가버린다.

 

 

 

2. @GeneratedValue(strategy = GenerationType.SEQUENCE)

 

 

- 시퀀스 오브젝트를 만들어 낸다.
- @SequenceGenerator(name = "MEMBER_SEQ_GENERATOR", sequenceName = "MEMBER_SEQ")
- 이와 같이 시퀀스 이름을 설정할 수도 있다.

- 특징 : 이 전략을 사용하면 영속성 컨텍스트에 영속(em.persist();)할 때, 매핑된 데이터베이스 시퀀스에서 시퀀스 값을 알아야 영속성 컨텍스트에 등록할 수 있다. (시퀀스 오브젝트는 DB가 관리하는 것이기 때문에 DB에 가봐야 알 수 있는 것이다.) 


그래서 em.persist(); 할 때 DB에서 매핑된 데이터베이스 시퀀스에서 다음 시퀀스값을 먼저 얻어오고(call next value for SEQUENCE 하여 다음 시퀀스를 얻는 로그를 볼 수 있다.) 그 다음에 영속성 컨텍스트에 등록한다. 
영속성 컨텍스트에 등록만을 한 것이기 때문에 트랜젝션을 커밋하기 전까지는 실제 쿼리가 날아가지 않는다.

 


* 의문점 : 그러면 영속성 컨텍스트에 영속을 할때마다 DB에 들러야하면 네트웤을 계속 타서 성능이 저하되는 것이 아닐까?

 

-> allocationSize 옵션을 이용한다.
allocationSize = 50 으로 지정해놓으면 call next value을 한번 하면 미리 50개의 사이즈를 디비에 올려놓고(DB는 시퀀스가 51번 부터 된다.) 메모리에서 1씩 사용하는 것이다. 50개 다쓰면 다시 50개 가져와서 사용한다. (DB는 시퀀스가 101번 부터 된다.) 

 

이 때, 로그를 보면 call next value가 2번 호출된 것을 볼 수 있다. 이유는 첫 번째 call은 처음 DB에 들렀을 때 DB내부적으로 시퀀스 값을 1증가시켜서 가져온 값이고, 두 번째 call은 allocationSize가 50이기 때문에 DB시퀀스를 50까지 증가시켜 놓는 작업 위한 call이라고 이해하면 된다.

 

 

3. @GeneratedValue(strategy = GenerationType.TABLE)

 

 

- 키 생성 전용 테이블을 하나 만들어서 데이터베이스 시퀀스를 흉내내는 전략이다.
- 장점 : 모든 데이터베이스에 적용 가능하다.
- 단점 : 성능이 떨어진다.
- 특징 : SEQUENCE전략과 비슷하게 allocationSize옵션을 동일한 방식으로 사용할 수 있다. 

 

 

 

4. 권장하는 식별자 전략

- 기본키 제약 조건 : null 아니고 유일, 변하면 안된다. 미래까지 이 조건을 만족하는 자연키는 찾기 어렵다. 대리키(대체키)를 사용하자.
- 권장 : Long형 + 대체키 + 키 생성전략 사용

 

 

JPA에서 가장 중요한 2가지가 있다. 

1. 객체와 관계형 데이터베이스 매핑하기 (Object Relational Mapping)

2. 영속성 컨텍스트

영속성 컨텍스트에 대해서 공부한 내용을 정리해보자.

 

 

 

본 정리 내용은 "김영한님의 자바 ORM 표준 JPA 프로그래밍 - 기본편" 을 듣고 정리한 내용이며, 설명에 필요한 사진 중 중요한 사진이라고 생각한 사진을 강의자료에서 가져왔음을 출처로서 밝힙니다.

 

 

 

영속성 컨텍스트란?

- 번역을 하자면, "엔티티를 영구 저장하는 환경" 이라는 뜻이다.
- 영속성 컨텍스트는 논리적인 개념으로 눈에 보이지 않는다. 엔티티 매니저(Entity Manager)를 통해서 영속성 컨텍스트에 접근한다.
- 엔티티 매니저와 영속성 컨텍스트가 1:1 생성된다.

 

 

 

 

 

엔티티의 생명주기

 

1. 비영속(new/transient)

- 영속성 컨텍스트와의 전혀 관계과 없는 새로운 상태를 뜻한다.

- 그냥 객체를 생성해서 세팅만 한 상태, 이거는 JPA와 전혀 관계가 없는 상태이다.

 

 

 

 

2. 영속(managed) 

- 영속성 컨텍스트에 관리되는 상태를 뜻한다. 

 

- 객체를 생성해서 엔티티매니저를 얻어와서 em.persist(memebr); 하면 객체를 영속 컨텍스트에 member가 들어가면서 영속 상태가 된다. 

- 중요한점은 em.persist(memebr); 한다고 DB에 쿼리가 날라가는 것이 아니다. ts.commit(); 하는 순간 날라간다.

 

 

 

3. 준영속(detached) 

- 영속성 컨텍스트에 저장되었다가 분리된 상태이다.

 

 

 

 

 

 

4. 삭제(removed) 

- 객체가 삭제되는 상태이다.

 

 

 

 

 

 

 

 

영속성 컨텍스트의 이점

 

1. 1차 캐시

 

 

- em.persist(memebr); 하여 영속 상태가 되었다고 가정하자. 영속성 컨텍스트(EntityManager)에는 1차 캐시가 있다. 1차 캐시에는 @Id로 우리가 매핑한 pk값이 key값으로, member가 value값으로 저장이 된다. (Map형태로 저장이 된다.)

 

 

 

 

 

- 이후 조회를 한다고 가정하여 em.find(Member.class, "member1"); 하면 JPA는 DB를 먼저 찾는게 아니라 우선 영속성 컨텍스트에서 1차캐시를 뒤져서 캐시값을 조회하여 일치하면 바로 Entity값을 꺼내온다.

- 그런데 만약 em.find(Member.class, "member2"); 하여 1차 캐시에 없는 것을 조회하면, 1차적으로 1차 캐시에서 찾고 없으면 DB를 조회한다. 그 후 DB에서 조회한 member2를 1차 캐시에 다시 저장한다. 그 이 후 member2를 반환한다.

 

- EntityManager는 트랜젝션단위로 생성하고 지우기 때문에 1차 캐시로 인해 엄청난 이득을 보는것은 아니고, 찰나의 순간에만 이득이다.

 

 

 

2. 영속 엔티티의 동일성 보장

- 1차 캐시로 반복 가능한 읽기(REPEATABLE READ) 등급의 트랜잭션 격리 수준을 데이터베이스가 아닌 애플리케이션 차원에서 제공한다. (마치 자바 컬렉션에서 같은 레퍼런스를 바라보는 객체를 == 비교하면 같은 것처럼) 

 

- 출력의 결과는 true가 나온다. 이유는 1차 캐시를 사용하기 때문이다. em.find(Member.class, "member100");을 하면 먼저 1차 캐시를 뒤지지만 1차 캐시에는 해당 엔티티가 없으니 DB를 조회한다. DB에서 가져온 객체를 1차 캐시에 저장한 후 return 한다. (member1)

다시한번 em.find();를 했을 때에도 1차 캐시를 검색하는데 아까 위에서 member1에서 가져온 객체와 같은 객체가 캐시에 담겨있다. 그러므로 member2는 1차 캐시에서 꺼내온다. 같은 객체를 == 비교하면 당연히 참조값이 같기 때문에 출력결과는 true가 나온다. 

여기에서 알 수 있었던 점은 em.persist(); 해야만 무조건 영속성 컨텍스트에 저장되는 것이 아니라, 영속성 컨텍스트에 없는 객체라면 em.find();했을 때에도 DB에서 가져온 객체가 1차 캐시에 들어가서 영속상태가 될 수 있다.

 

 

 

3. 엔티티 등록 - 트랜잭션을 지원하는 쓰기 지연 (Buffering)

 

 

- em.persist(memebrA); 할 때까지는 Insert sql을 데이터베이스에 보내지 않는다. transaction.commit(); 하여 커밋하는 순간 데이터베이스에 쿼리를 날린다. 

 

- 영속 컨텍스트 안에는 "쓰기 지연 SQL저장소" 라는게 있다. persist(memberA);하면 memberA가 1차 캐시에 저장이 됨과 동시에 INSERT SQL을 생성하여 쓰기 지연 SQL저장소에 쌓는다. 그 후에 또 한번 persist(memberB); 하면 1차캐시에 저장이 되면서 SQL을 쓰기 지연 SQL저장소에 쌓는다.
transaction.commit();을 하는 시점에 쓰기 지연 SQL저장소에 있던 SQL들이 날아간다. (JPA에서는 flush()라고 한다. 플러시는 아래에서 설명)

 

 

 

4. 엔티티 수정 - 변경감지

 

- 데이터를 변경하는 상황에서 Member member = em.find(Member.class, "member100"); 하여 영속 엔티티를 조회하고, member.setName("hello"); 와 같이 이름을 변경하였다. update쿼리를 날리려면 em.update(member); 와 같은 코드를 사용하거나 em.persist(member); 하여 영속성 컨텍스트에 영속을 다시 해주어야 할까? 

 

 


아니다. 우리가 자바 컬렉션 이용할 때 get하여 객체를 얻고 변경 후에 다시 add하여 집어 넣어주나? JPA에서도 그렇게 하지 않아도 된다. 어떻게 이런일이 가능할 수 있을까? 

 

 

     4.1. Dirtiy Checking

- 이유는 바로 Dirty Checking때문이다.

 

 

- transaction.commit();을 하면 flush();가 호출이 되고, 엔티티와 스냅샷을 비교한다.

스냅샷은 1차 캐시안에 있는데 값을 최초로 읽어온 시점, 영속 컨텍스트에 값을 집어 넣든 DB에서 가져오든 최초 시점의 상태를 스냅샷으로 떠놓는 것이다. 비교하여 변경을 감지하고, 변경이 있으면 UPDATE SQL을 쓰기지연 SQL저장소에 쌓는다. 그 후 쿼리를 날려서 DB에 변경값을 반영(commit)한다.

 

- 간단하게 변경감지 메커니즘은 다음과 같이 정리할 수 있다.

플러시 발생 -> 변경감지(Dirty Checking) -> 수정된 엔티티 쓰기 지연 SQL저장소에 등록 -> 쓰기 지연 SQL 저장소의 쿼리를 데이터베이스에 전송(등록, 수정, 삭제쿼리)

 

 

 

 

 

플러시 (flush)

- 플러시를 하면 영속성 컨텍스트의 변경내용을 데이터베이스에 반영한다.
- flush하면 1차 캐시나 영속성 컨텍스트가 지워질까? 아니다. 쓰기 지연 SQL 저장소에 있는 쿼리들을 DB에 반영하는 과정일 뿐이다. (영속성 컨텍스트의 변경내용을 데이터베이스에 동기화하는 것이다.)

 

 

- 영속성 컨텍스트를 플러시 하는 방법

1. em.flush() - 직접호출

 

- em.flush(); 직전까지의 상황은 member객체에 값을 set해주고 em.persist(member);를 하여 영속상태로 만들어주었다. 그러면 영속성 컨텍스트 내부에서는 1차 캐시에 member를 저장하고, INSERT SQL을 만들어서 쓰기 지연 SQL 저장소에 쿼리를 쌓고 있는 상태이다. 

ts.commit();하면 DB로 쿼리가 날라가는 상황인데, 커밋명령을 하기 전에 em.flush(); 를 직접 호출하여 쿼리를 바로 DB에 날릴 수 있는 것이다.

 

 

2. 트랙잭션 커밋 - 자동 호출

 

3. JPQL 쿼리 실행 - 자동 호출

 

 

- 이 상황에서 memberA, memberB, memberC가 조회가 될까? 당연히.. 아직 flush가 된 상황이 아니라 안된다. 그래서 JPQL쿼리를 실행하면 JPA는 자동으로 flush를 해주고 쿼리를 날린다. 

 

 

 

 

 

 

준영속상태

- 영속상태의 엔티티가 영속성 컨텍스트에서 분리(detach) 한 상태이다.


- 준영속 상태로 만드는 방법

1. em.detach(entity) - 특정 엔티티만 준영속상태로 전환

 

- em.find(); 해서 1차 캐시를 먼저 검색하지만 없다. DB에서 10L을 pk값으로 가지는 객체를 가져온다. 그 후에 em.detach(member);를 해주어서 준영속상태로 만들었다. 이렇게 하면 JPA가 관리하지 않는(영속성 컨텍스트에서 분리된) 엔티티가 되기 때문에 1차 캐시에서도 삭제된다.

 


2. em.clear() - 모든 엔티티를 준영속상태로 전환

- 영속성 컨텍스트에 있는 모든 엔티티를 준영속 상태로 전환한다.

 

3. em.close() - 영속성 컨텍스트를 종료

- 영속성 컨텍스를 종료한다.

 

 

스프링 빈의 scope는 2가지 종류가 있다.

 

  • Singleton(싱글톤) : 기본 스코프, 스프링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 스코프이다.
  • ProtoType(프로토타입) : 스프링 컨테이너는 프로토타입 빈의 생성과 의존관계 주입까지만 관여하고 더는 관리하지 않는 매우 짧은 범위의 스코프이다.

 

싱글톤 스코프는 스프링 빈의 디폴트이고 스프링 컨테이너가 싱글톤으로 관리한다는 것을 예전 포스팅에서 알아보았다.

 

https://programmingrecoding.tistory.com/55?category=846369 

 

싱글톤 패턴과 스프링 컨테이너

싱글톤 패턴이란? - 클래스의 인스턴스가 딱 1개만 생성되는 것을 보장하는 디자인 패턴이다. - 자바가 뜰 때,  SingletonPattern클래스의 static 영역에 new 가 있으니까 자기자신을 생성해서 instance변

programmingrecoding.tistory.com

 

프로토 타입 스코프에 대해서 자세히 알아보자.

 

 

 

 

 

 

프로토 타입 스코프

싱글톤 스코프의 빈을 조회하면 스프링 컨테이너는 항상 같은 인스턴스 스프링 빈을 반환한다. 반면 프로토타입 스코프를 스프링 컨테이너에 조회하면 스프링 컨테이너는 항상 새로운 인스턴스를 생성해서 반환한다. 

스프링 컨테이너는 프로토타입 빈을 생성하고, 의존관계 주입, 초기화까지만 생성한 후 클라이언트에게 빈을 반환하고 그 이후엔 스프링 컨테이너는 생성된 프로토타입 빈을 관리하지 않는다. 프로토타입 빈을 관리할 책임은 프로토타입 빈을 받은 클라이언트에 있다. 그래서 @PreDestory 같은 종료 메서드가 호출되지 않는다. 

 

 

 

PrototypeBeanTest 테스트 코드

 

간단하게 테스트 코드로 프로토타입 스코프 빈의 생성과 소멸을 살펴보자.

31줄의 ProtoTypeBean이라는 빈을 만드는데 @Scope("prototype") 애노테이션을 이용하여 빈으로 등록될 때 프로토타입 스코프 속성을 가지게 해주었다. 생성과 소멸을 보기 위해서 @PostContruct와 @PreDestory 애노테이션을 이용하여 init메서드와 close메서드를 각각 만들어주었다.

 

19번줄에서 스프링 컨테이너를 생성하고 getBean하여 빈을 가지고 와서 2번을 찍어보았다.

 

 

 

 

분명 로그 마지막줄에 Closing~~ 로그가 찍혀서 스프링 컨테이너가 소멸이 되었지만 close메서드는 호출되지 않았다.

 

일반적인 싱글톤 스코프의 스프링 빈 생명주기는 다음과 같다.

"스프링 컨테이너 생성" -> "스프링 빈 생성" -> "의존관계 주입"
-> "초기화 콜백" -> "사용" -> "소멸전 콜백" -> "스프링 종료"

 

하지만, 프로토타입 스코프의 스프링 빈은 초기화 콜백 후, 스프링 컨테이너에서 관리하지 않기 때문에 소멸전 콜백의 과정은 일어나지 않는 것이다. 만약(거의 일어나지 않겠지만), 프로토타입 스코프인 빈의 소멸전 콜백을 사용해야 한다면 close() 메서드를 직접 호출해서 사용해야 한다.

 

 

또 한, 각각 protoTypeBean1과 protoTypeBean2는 싱글톤처럼 한개의 빈을 공유한 것이 아닌 서로 다른 빈 객체이다. 로그를 보면 마지막에 주소값이 서로 다른것을 확인할 수 있다. 스프링 컨테이너에게 요청할 때마다 새로 생성되는 것이다.

 

 

 

 

 

싱글톤에서 프로토타입 빈을 사용할 때 (문제점)

스프링 컨테이너에 프로토타입 스코프의 빈을 요청하면 항상 새로운 객체 인스턴스를 생성해서 반환한다. 그런데, 문제는 싱글톤 빈과 프로토타입 빈을 함께 사용하면 의도한대로 잘 동작하지 않는다..

 

 

김영한님의 스프링 핵심 원리 - 기본편 강의자료에 너무나도 이해가 쉽게 영한님이 그려놓으신 그림이 있어서 이해를 위해 사진을 가져왔다.

출처 : <인프런> 스프링 핵심 원리 기본편 - 김영한 

 

1. clientBean은 의존관계 자동주입을 사용한다. 주입 시점에 스프링 컨테이너에 프로토타입 빈을 요청한다.

2. 스프링 컨테이너는 프로토타입 빈을 생성해서 'clientBean'에 반환한다. 프로토 타입 빈의 count필드 값은 0이다. 이제 프로토타입 빈은 스프링 컨테이너의 손을 떠났고 'clientBean'에서 관리한다.(프로토타입 빈을 내부 필드에 보관한다. 정확히는 참조값을 보관한다.)

 

 

 

3. 클라이언트 A는 clientBean을 스프링 컨테이너에 요청하여 받는다. clientBean은 싱글톤이므로 항상 같은 clientBean이 반환된다.

4. 클라이언트 A는 'clientBean.logic()'을 호출한다.

5. clientBean은 prototypeBean의 'addCount()'를 호출해서 프로토타입 빈의 count를 증가한다. count는 1이 된다.

 

 

 

 

 

6. 클라이언트 B는 clientBean을 스프링 컨테이너에 요청하여 받는다. clientBean은 싱글톤이므로 항상 같은 clientBean이 반환된다.

7. clientBean이 내부에 가지고 있는 프로토타입 빈은 이미 과거에 주입이 끝난 빈이다. 주입 시점에서 스프링 컨테이너에 요청해서 프로토타입 빈이 새로 생성된 것이지 사용할 때마다 새로 생성되는 것이 아니다. 

8. 클라이언트 B는 'clientBean.logic()'을 호출한다.

9. clientBean은 prototypeBean의 'addCount()'를 호출해서 프로토타입 빈의 count를 증가한다. count는 2이 된다.

 

 

 

 

위의 그림을 코드로 이해해보자.

 

SingletonBean과 PrototypeBean

 

싱글톤빈의 @Scope 애노테이션은 디폴트 값이기 때문에 생략해주었다. SingletonBean에서 PrototypeBean을 의존관계 자동주입한다.

 

 

 

 

스프링 컨테이너가 만들어지고, SingletonBean의 의존관계 자동주입된 PrototypeBean은 주입 시점에 스프링 컨테이너에 프로토타입 빈을 요청한다. 빈의 스코프가 프로토타입이기때문에 우리는 singleton1과 singleton2객체의 logic메서드를 각각 호출한 결과가 같은 값인 1임을 기대했지만 logic1은 1, logic2는 2라는 결과를 얻었다.

 

위에서 설명하였지만, 내부에 가지고 있는 프로토타입 빈은 이미 과거에 주입이 끝난 빈이다. 주입 시점에서 스프링 컨테이너에 요청해서 프로토타입 빈이 새로 생성된 것이지 사용할 때마다 새로 생성되는 것이 아니다

 

 

 

우리는 어떻게 기대하는 값을 얻어낼 수 있을까?

 

 

 

 

ObjectFactory, ObjectProvider

지정한 빈을 컨테이너에서 대신 찾아주는 DL(Dependency Lookup)서비스를 제공하는 인터베이스인 ObjectFactory나 ObjectProvider를 사용하면 된다.

 

우리는 외부에서 의존관계를 주입해주는 것을 Dependency Injection이라고 불렀다. 보통 @Autowired 애노테이션을 사용하여 스프링 컨테이너가 생성된 후, 의존관계 자동주입을 할 수 있도록 해주었었다.

Dependency Lookup은 의존관계를 내부에서 주입받는 것이 아니라 직접 필요한 시점에 의존관계를 찾는 것을 말한다. 

 

 

 

 

이와 같이 의존관계 자동주입을 할 PrototypeBean을 ObjectProvider혹은 ObjectFactory인터페이스로 만들고, 실제 이 빈이 사용되는 로직에서 35번째 줄과 같이 getObject메서드를 이용해서 불러오면 된다. 자동으로 의존관계를 주입해주는 것이 아니라 클라이언트가 필요시에 빈을 가져와서 의존관계를 찾았기 때문에 DI가 아니라 DL 인 것이다. 이 때, DL을 해줌으로써 우리는 빈의 생성을 지연할 수 있었다.

 

ObjectFactory는 getObject기능만 있는 단순한 버전이고 ObjectProvider는ObjectFactory상속, 옵션, 스트림처리 등 편의 기능이 추가된 것이다. 둘 다 패키지를 보면 스프링 프레임워크에 의존하고 있다.

 

그래서 스프링 프레임워크에 의존하지 않는 자바표준(JSR-330)을 사용하는 방법이 있다.

Provider<T> 라는 인터페이스 인데 javax.inject 패키지에 있는 인터페이스 이다. 이 인터페이스를 사용하려면 build.gradle에 라이브러리를 추가해주어야 한다. ( implementation 'javax.inject:javax.inject:1' )

get() 메서드 하나로 기능이 매우 단순하며 스프링 프레임워크에 의존하지 않는다는 장점이 있다.

 

 

 

 

 

 

웹 스코프

웹 스코프들은 웹 환경에서만 동작한다. 프로토 타입과는 다르게 스프링이 해당 스코프의 종료시점까지 관리를 해준다. 따라서 종료메서드가 호출이 된다.


 "request" : HTTP 요청하나가 들어오고 나갈때 까지 유지되는 스코프, 각각의 HTTP요청마다 별도의 빈 인스턴스가 생성되고 관리된다.
 "session" : 웹 세션이 생성되고 종료될 때까지 유지되는 스코프, HTTP Session과 동일한 생명주기를 가지는 스코프.
 "application" : 웹 서블릿 컨텍스트(ServletContext)와 동일한 생명주기를 가지는 스코프.
 "websocket" : 웹 소켓과 동일한 생명주기를 가지는 스코프.

 

 

 

이중에서 request 스코프에만 대해서 예제를 통해서 자세히 알아보자. 나머지 스코프도 범위만 다르지 동작하는 메커니즘은 거의 비슷하다.

 

 

 

클라이언트A와 클라이언트B가 각각 다른 HTTP request를 요청하였다. 컨트롤러에서 myLogger를 요청하는데 myLogger는 request 스코프이다. 이때 클라이언트A 전용 빈, 클라이언트B 전용 빈이 각각 생성되게 되는 것이다.

 

 

 

MyLogger

 

로그를 출력하기 위한 MyLogger 클래스이다. @Scope(value = "request") 애노테이션을 지정하여 스코프를 request로 설정하였다. 이제 이 빈은 HTTP요청당 한개씩 생성되고, HTTP요청이 끝나는 시점에 소멸된다. 

@PostContruct 초기화 메서드를 사용하여 빈이 생성되고 자동주입이 끝난 직후에 uuid를 랜덤하게 지정하였다. java.util.UUID의 uuid를 사용하였다. randoUUID().toString()하면 고유한 uuid값을 만들어준다.

 

url은 이 빈이 생성되는 시점에는 알 수 없으므로 setter를 이용해서 외부에서 주입받는다. 

 

 

 

 

MyLoggerService

 

Service이다.

@RequiredArgsContructor 롬복 애노테이션을 이용하여 myLogger를 생성자 파라메터로 자동주입한다.

logic메서드를 만들어서 MyLogger클래스의 log메서드를 호출해준다.

 

 

MyLoggerController

 

Controller이다.

@RequiredArgsContructor 롬복 애노테이션을 이용하여 myLoggerService와 myLogger를 생성자 파라메터로 자동주입한다. HTTP요청이 들어오면 그 URL을 25번줄의 url변수에 담아 myLogger에 set해주고, myLogger의 log메서드와 myLoggerService의 logic메서드를 각각 호출해본다.

 

(참고로 requestURL을 MyLogger에 저장하는 부분은 컨트롤러 보다는 공통처리가 가능한 스프링 인터셉터나 서블릿 필터 같은 곳을 활용하는 것이 좋다)

 

 

 

오류가 발생하였다. 

이유는 스프링 어플리케이션을 실행하는 시점에 싱글톤 빈은 생성하여 주입이 가능하지만, request스코프인 빈은 HTTP요청이 들어와야 생성되기 때문에 아직 생성되지 않았다. 아직 클라이언트의 요청이 들어오지 않았는데 컨트롤러의 저 메서드를 타게 되면 request스코프인 myLogger를 호출하게 되어 오류가 발생하는 것이다. 

 

 

 

 

 

2가지 해결방법에 대해서 알아보자.

 

 

 

 

1. Provider의 활용 (ObjectFactory, ObjectProvider 도 마찬가지)

Provider를 사용한 DL

 

Provider덕분에 myLoggerProvider.get()을 호출하는 시점까지 request scope 빈의 생성을 지연할 수 있다.

myLoggerProvider.get()을 호출하는 시점에는 HTTP요청이 진행중이므로 request scope빈의 생성이 정상 처리된다. 

 

 

 

 

 

 

2.  프록시 방식

@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)

 

위와 같이 request스코프를 지정해줄 때, proxyMode를 TARGET_CLASS로 지정해주는 방법이 있다.

적용대상이 클래스이면 TARGET_CLASS, 인터페이스이면 TARGET_INTERFACES를 써주면 된다. 

 

이렇게 옵션을 지정하면 MyLogger의 가짜 프록시 클래스를 만들어두고 HTTP request와 상관 없이 가짜 프록시 클래스를 다른 빈에 미리 주입해 둘 수 있다. 

스프링 컨테이너는 CGLIB라는 바이트 코드를 조작하는 라이브러리를 사용하여 MyLogger를 상속받은 가짜 프록시 객체를 생성한다. 의존관계 주입에서도 이 가짜 프록시 객체가 주입이 된다.

 

가짜 프록시 객체는 요청이 오면 그때 내부에서 진짜 빈을 요청하는 위임 로직이 들어있다. 가짜프록시 객체는 내부에 진짜 myLogger를 찾는 방법을 알고 있다. 클라이언트가 myLogger.logic()을 호출하면 사실은 가짜프록시 객체의 메서드를 호출한 것이다. 가짜프록시 객체는 request 스코프의 진짜 myLogger.logic()를 호출한다.
가짜프록시 객체는 원본 클래스를 상속 받아서 만들어졌기 때문에 이 객체를 사용하는 클라이언트 입장에서는 사실 원본인지 아닌지도 모르게, 동일하게 사용할 수 있다. (다형성)

 

이 CGLIB라이브러리는 스프링 빈이 자동으로 싱글톤으로 유지될 수 있도록 할 때에도 사용되었었다. (이전 포스팅 참고)

 

 

 

특징 정리

-> 프록시 객체 덕분에 클라이언트는 마치 싱글톤 빈을 사용하듯이 편리하게 request scope를 사용할 수 있다.
사실 Provider를사용하든, 프록시를 사용하든 핵심 아이디어는 진짜 객체 조회를 꼭 필요한 시점까지 지연처리 한다는 점이다. 단지 애노테이션 설정 변경만으로 원본 객체를 프록시 객체로 대체할 수 있다. 이것이 바로 다형성과 DI 컨테이너가 가진 큰 강점이다. 꼭 웹스코프가 아니어도 프록시는사용할수있다. 

 

 

주의점

-> 마치 싱글톤을 사용하는 것 같지만 다르게 동작하기 때문에 결국 주의해서 사용해야 한다. 이런 특별한 scope는 꼭 필요한 곳에만 최소화해서 사용하자. 무분별하게 사용하면 유지보수 하기 어려워진다.

 

데이터베이스 커넥션 풀이나 네트워크 소켓처럼 애플리케이션 시작 시점에 필요한 연결을 미리 해두고, 애플리케이션 종료 시점에 연결을 모두 종료하는 작업을 진행하려면 객체의 초기화와 종료작업이 필요하다. 

(데이터베이스 커넥션 풀 같은 경우, 미리 애플리케이션 서버가 올라올 때 커넥션을 미리 연결을 해놓는다.)

 

먼저, 스프링 빈의 생명주기를 살펴보자.

 

"스프링 컨테이너 생성" -> "스프링 빈 생성" -> "의존관계 주입"
-> "초기화 콜백" -> "사용" -> "소멸전 콜백" -> "스프링 종료"  

 

 

초기화 콜백 : 빈이 생성되고, 빈의 의존관계 주입이 완료된 후 호출
소멸전 콜백 : 빈이 소멸되기 직전에 호출

 

초기화 콜백과 소멸전 콜백이 구분되어 있기 때문에 필요 시, 콜백 메서드를 통해서 시작 지점과 종료 지점의 처리를 각각 해줄 수 있는 것이다. 

 

 

 

그런데, 

커넥션 풀이나 네트워크 소켓처럼 생성될 때 해야하는 처리라면 객체 생성과 동시에 생성자에서 초기화 처리할 수 있지 않을까?

 

 

객체의 생성과 초기화를 분리하자.

 

생성자는 필수 정보(파라메터)를 받고, 메모리를 할당하여 객체를 생성하는 책임을 가진다. 

반면에 초기화는 이렇게 생성된 값들을 활용해서 외부 커넥션을 연결하는 등 무거운 동작을 수행한다.
따라서 생성자 안에서 무거운 초기화 작업을 함께 하는 것 보다는 객체를 생성하는 부분과 초기화를 하는 부분을 명확하게 나누는 것이 유지보수의 관점에서도 좋으며 단일책임 원칙(SRP)를 지키는 설계라고 할 수 있다. 물론 초기화 작업이 내부 값들만 약간 변경하는 정도의 간단한 경우에는 생성자에서 한번에 처리하는 것이 나을 수도 있다.

 

 

 

 

 

 

 

빈 생명주기 콜백

 

 

 

BeanLifeCycle

 

빈 생명주기 콜백 방법들을 알아보기 위해 위와 같은 클래스를 만들었다.

 

 

 

 

LifeCycleTest

 

테스트 파일에 LifeCycleConfig를 만들어서 BeanLifeCycle을 빈으로 등록하였다. 그리고 등록하는 빈에 서 setter를 이용해서 url을 넣어주었다. 이렇게 하면 어떻게 나올까?

 

 

 

결과

 

 

등록될 빈인 beanLifeCycle() 에서 BeanLifeCycle bean = new BeanLifeCycle(); 로 생성자를 호출하여 객체를 만든 후에 setUrl을 해주었기 때문에 생성자를 호출했을 당시에는 url이 null 값이라 위와 같은 결과를 얻게 되는 것이다. 

connect와 disconnect를 빈 등록 시점(의존관계 주입 이후)과 빈 소멸 직전 시점에 적절하게 콜백하는 방법에 대해 한개씩 알아보자.

 

 

 

 

 

 

 

 

 

 

1. 인터페이스 InitializingBean, DisposableBean

 

 

BeanLifeCycle에 InitializingBean, DisposableBean 인터페이스를 implements 해준다.

 

 

Override된 메서드들을 재정의한다.

 

 

 

테스트 결과

 

 

 

- 이 인터페이스는 스프링 전용 인터페이스이다. 해당 코드가 스프링 전용 인터페이에 의존하게 된다. 
- 초기화, 소멸 메서드의 이름을 변경할 수 없다. 
- 내가 코드를 고칠 수 없는 외부 라이브러리에 적용할 수 없다.
- 인터페이스를 사용하는 초기화, 종료방법은 스프링 초창기에 나온 방법들로 지금은 거의 사용하지 않는다.

 

 

 

 

 

 

2. 빈 등록 초기화, 소멸 메서드

 

 

 

BeanLifeCycle에 빈 생성 이후 시점과 빈 소멸 직전 시점에 호출될 메서드들을 각각 정의해준다.

 

 

 

@Bean애노테이션을 쓸 때 initMethod와 destroyMethod를 각각 등록해준다.

 

 

테스트 결과

 

- 메서드 이름을 자유롭게 줄 수 있다.
- 스프링 빈이 스프링 코드에 의존하지 않는다.
- 코드가 아니라 설정 정보를 사용하기 때문에 코드를 고칠 수 없는 외부 라이브러리에도 초기화, 종료 메서드를 적용할 수 있다.

 

 

종료 메서드 추론

- @Bean의 destroyMethod속성에는 아주 특별한 기능이 있다.
- 라이브러리는 대부분 close, shutdown 이라는 이름의 종료 메서드를 사용한다. 
- @Bean의 destroyMethod는기본값이 (inferred)(추론) 으로 등록되어 있다.
- 이 추론 기능은 close, shutdown 라는이름의 메서드를 자동으로 호출해준다. 이름 그대로 종료 메서드를 추론해서 호출 해준다.
- 따라서 직접 스프링 빈으로 등록하면 종료 메서드는 따로 적어주지 않아도 잘 동작한다. 추론 기능을 사용하기 싫으면destroyMethod="" 처럼 빈 공백을 지정하면 된다.

 

 

 

 

 

 

3. @PostContruct, @PreDestory

 

 

 

BeanLifeCycle에 빈 생성 이후 시점과 빈 소멸 직전 시점에 호출될 메서드를 만들고 그 메서드에 @PostContruct, @PreDestroy 애노테이션을 각각 붙여주면 된다.

 

 

 

테스트 결과

 

 

 

- 패키지를보면 'javax.annotaion.PostContruct'이다. javax는 스프링에 종속된 기술이 아니라 JSR-250이라는 자바 표준이다. 따라서 스프링이 아닌 다른 컨테이너에서도 동작한다.
- 컴포넌트 스캔과 잘어울린다.
- 유일한 단점은 외부 라이브러리에는 적용하지 못한다는 것이다. 외부 라이브러리를 초기화, 종료 해야하면 @Bean의 기능을 사용하자 (2번)

다양한 의존관계 주입 방법

1. 생성자 주입

- 생성자 호출시점에 딱 한번만 호출되는 것이 보장이 된다. 한번 세팅하면 그 다음부터는 세팅할 수 없도록 할 수 있다는 것이다.
- 불변, 필수 의존관계에 사용된다.
- 생성자가 딱 1개만 있으면 @Autowired를 생략할수도 있다.

 


2. 수정자 주입 (setter주입)

- setter라고 불리는 필드의 값을 변경하는 수정자 메서드를 통해서 의존관계를 주입하는 방법이다.
- 선택, 변경 가능성이 있는 의존관계에 사용된다.
- @Autowired의 기본동작은 주입할 대상이 없으면 오류가 발생한다. 주입할 대상이 없어도 동작하게 하려면 @Autowired(required = false)로 지정하면 된다.

 


3. 필드 주입

- 필드 주입을 사용하면 외부에서 변경이 불가능해서 테스트하기 힘들다는 단점이 있어 최근에는 사용하지 않는다.
- 애플리케이션의 실제 코드와 관계없는 테스트코드에 사용한다.
- 스프링 설정을 목적으로 하는 @Configuration 같은 곳에서만 특별한 용도로 사용한다.


4. 일반 메서드 주입

- 일반 메서드를 통해서 주입을 받을 수 있다.
- 한꺼번에 여러 필드를 주입 받을 수 있다. 일반적으로는 잘 사용하지 않는다.

 

 

 

 

 

 

 

 

@Autowired 옵션처리

1. @Autowired(required = false)

- 자동 주입할 대상이 없으면 수정자 메서드 자체가 호출이 안된다.

 

 

@Autowired(required = false)의 예시

 

- 위의 setNoBean1 메서드의 파라메터에 있는 Member는 스프링 빈에 등록된 객체가 아니다. 그때 그냥 @Autowired를 한다면 스프링 빈이 아닌 객체를 의존관계 자동주입을 하려고 했으니 오류가 발생한다. 아마 NoSuchBeanDefinitionException이 나올 것이다. required = false 옵션을 넣어주면 호출 자체가 안되기 때문에 Exception이 터지지 않고 아예 print값이 찍히지 않는다.

 

 

 

 

 


2. @Nullable

- 자동 주입할 대상이 없으면 null이 입력된다.

 

@Nullable의 예시

- 스프링 빈으로 등록되지 않은 것을 아는데 찍어주어야 할 상황이 생길 수도 있을 것이다. 위의 setNoBean2처럼 파라메터가 한개라면 그럴일이 없지만 예를 들어 파라메터가 3개인데 마지막 변수는 스프링 빈이 아니지만 테스트를 위해서 null이라도 찍어보고 싶다. 그럴 때 사용할 수 있다.

 

 

- 결과는 이처럼 null로 찍힌다.

 

 

 

 

 

 


3. Optional<>

- 자동 주입할 대상이 없으면 Optional.empty가 입력된다.

 

Optional&amp;amp;lt;&amp;amp;gt;의 예시

- 자바 8 문법인 Optional을 사용할 수도 있다. Optional은 “존재할 수도 있지만 안 할 수도 있는 객체”, 즉, “null이 될 수도 있는 객체”를 감싸고 있는 래퍼 클래스이다. 

 

 

- 결과는 이처럼 Optional.empty로 얻을 수 있다.

 

 

 

 


@Nullable, Optional<>은 스프링 전반에 걸쳐서 지원이 된다. 예를 들어서 생성자 자동 주입에서 마지막 파라메터가 스프링빈에 없는데 생성은 하고 싶다? 그러면 넣을 수도 있는 것이다.

 

 

 

 

 

 

 

 

 생성자 주입을 선택해야 하는 이유

최근에는 스프링을 포함한 DI프레임워크 대부분이 생성자 주입을 권장한다.

 

1. 불변

- 대부분의 의존관계 주입은 한번 일어나면 애플리케이션 종료시점까지 의존관계를 변경할 일이 없다. 오히려 대부분 의존관계는 애플리케이션 종료전까지 변하면 안된다.
- 수정자 주입을 사용한다고 하면 메서드를 public으로 열어두어야 하고 누군가가 실수로 변경할 수도 있기 때문에 변경하면 안되는 메서드를 열어두는 것은 좋은 설계 방법이 아니다.

 

2. 누락

- 프레임워크 없이 순수한 자바 코드 단위 테스트 하는 경우가 많은데 수정자 주입인 경우 setter를 통해 한개의 필드라도 값을 넣어주지 않으면 NullPointerException이 발생한다. 
생성자 주입을 사용하면 생성자를 통해서 값을 넣어야하는데 값이 누락되면 컴파일 에러로 자바 컴파일러단에서 해결할 수 있다.  또한, 생성자 주입을 사용하면 final 키워드를 사용할수 있다. 생성자에서 혹시라도 값이 설정되지 않는 오류를 컴파일 시점에서 막아줄 수 있다.

 

 

- 생성자 주입은 프레임 워크에 유지하지 않고 순수한 자바 언어의 특징을 잘 살리는 방법이다.
- 기본으로 생성자 주입을 사용하고, 필수값이 아닌 경우에는 수정자 주입 방식을 옵션으로 부여하면 된다. 생성자 주입과 수정자 주입을 동시에 사용할 수 있다.
- .. 그냥 항상 생성자 주입을 선택하자 !

 

 

 

 

 

 

 Lombok 롬복

 

- @Getter, @Setter

: POJO클래스에 이 어노테이션들은 붙여주면 알아서 게터 세터를 만들어준다.

 


- @ToString

: toString메서드를 Override해서 필드들을 출력해준다. 

클래스명(필드1명=필드1값,필드2명=필드2값,...)  와 같이 출력이 된다.

 


- @RequiredArgsConstructor
: final 키워드가 붙은 필드(Required한)를 파라메터로 하는 생성자를 자동으로 생성해준다.

 

@RequiredArgsConstructor 의 예시

 

 

 



최근에는 생성자 1개를 두고 @Autowired를 생략하는 방법을 주로 사용한다. 여기에 롬복 라이브러리에서 제공하는 @RequiredArgsConstructor 를 함께 사용하면 기능은 다 제공하면서 코드를 깔끔하게 사용할 수 있다.

 

 

 

 

 

 

 

 

조회 빈이 2개 이상일 때

1. @Autowired 필드 명 매칭

- @Autowired는 타입기반으로 DI를 한다. 만약에 타입이 같은 스프링 빈이 여러개 있다면, 필드명을 찾거나 또는 생성자의 파라메터의 변수 이름과 빈의 이름이 같을 경우 그 클래스를 주입해준다.

 

 

 

@Autowired 필드 명 매칭예시

- DiscountPolicy에는 rateDiscountPolicy와 fixDiscountPolicy 2개의 구현체가 있다. 같은 타입을 @Autowired하기 때문에 변수명을 discountPolicy로 하면 오류가 나지만 구현체의 이름과 맞춰서 rateDiscountPolicy로 하니까 오류가 나지 않는다.

 

 

 

 

 

2. @Qulifier -> @Qulifier끼리 매칭 -> 빈 이름 매칭

- @Qulifier는 빈 이름을 변경하는 것이 아니라 추가적인 구분자를 붙여주는 방법이다.
@Qulifier("name")으로 구분해준 빈은 생성자, 수정자, 필드 주입 시 앞에 @Qulifier("name")를 붙여주는 방식으로 빈을 찾아준다.
만약, @Qulifier("name") 에서 name이라는 구분자로 등록된 빈이 없다면, name이라는 이름의 스프링빈을 추가로 찾는다. 
헷갈리지 않아야 하는 것은 빈 이름을 @Qulifier로 바꾸는게 아니라 구분자를 붙여주는 것이고 그것으로 찾지 못했을 때 추가적으로 @Qulifier로 와 같은 이름의 빈을 검색한다는 것이다.

 

 

RateDiscountPolicy

 

RateDiscountPolicy에 @Qualifier("mainPolicy")라고 설정해주었다.

 

 

 

 

@Qualifier 예시

 

조회빈이 2개이상인 파라메터 앞에 @Qualifier("mainPolicy")를 붙여줌으로서 DiscountPolicy는 RateDiscountPolicy가 되었다.

 

 

 

 

 

 


3. @Primary 사용

- 우선순위를 정하는 방법이다. 여러개의 빈이 매칭이 되면 @Primary가 우선권을 가진다.

 



RateDiscountPolicy

 

RateDiscountPolicy에 @Primary 라고 설정해주었다.

 

 

 

@Primary 예시

 

조회 빈이 2개 이상이지만 @Primary 애노테이션 덕분에 오류가 나지 않고 DiscountPolicy의 구현체는 RateDiscountPolicy가 되었다.

 

 

 

 

 

 

 

그런데.

뭔가 이상하지 않은가..

 

 

우리는 지금까지 SOILD 원칙을 최대한 지키면서 객체지향적인 설계와 프로그램을 짜왔다. 

그런데, DiscountPolicy에 두 개의 빈이 찾아지기 때문에 우리는 특정 빈을 찾을 수 있도록 인자의 파라미터 이름을 수정한다던지(@Autowired 필드명 방식), @Qualifier이나 @Primary 애노테이션을 붙이기 위해서 클라이언트 코드를 직접 수정하였다. 이는 개방에는 열려있고 수정에는 닫혀있어야하는 OCP원칙을 어긋났다. 

또 한, DiscountPolicy를 각각의 방법으로 인해 RateDiscountPolicy라는 구체화에 의존하도록 명시하였기 때문에 추상화에 의존해야지 구체화에 의존하면 안된다DIP원칙을 어긋났다.

 

이는 김영한 님의 해당 문제에 대한 답변.

기존 구현 클래스의 애노테이션도 변경하지 않으면 더 좋겠지만, 이 부분까지는 컴포넌트 스캔의 한계입니다. @Bean을 사용하면 확실하게 되지만 약간은 불편하지요. 따라서 둘의 트레이드 오프로 이해하시면 됩니다.

 

 

 

 

 

 


@Primary, @Qualifier 활용

- 코드에서 자주사용하는 메인 데이터베이스의 커넥션을 획득하는 스프링 빈이 있고, 코드에서 특별한 기능으로 가끔 사용하는 서브 데이터베이스의 커넥션을 획득하는 스프링빈이 있다고 생각해보자. 메인 데이터베이스의 커넥션을 획득하는 스프링빈은@Primary를 적용해서 조회하는 곳에서 @Qualifier 지정없이 편리하게 조회하고, 서브 데이터베이스 커넥션 빈을 획득할때는 @Qualifier를 지정해서 명시적으로 획득하는 방식으로 사용하면 코드를 깔끔하게 유지할 수 있다. 물론 이때 메인 데이터베이스의 스프링빈을 등록할때 @Qualifier를 지정해주는 것은 상관없다.

 

@Primary, @Qualifier 우선순위

- 스프링은 항상 더 자세한 것이 우선순위를 가져간다. 자동보다는 수동이, 넓은 범위의 선택권보다는 좁은 범위의 선택권이 우선권이 높다. 따라서 @Qualifier가 우선권이 높다.

 

 

 

 

 

 

 

 조회한 빈이 모두 필요할 때

 

조회한 빈이 모두 필요할 때는 어떻게 각각의 빈들을 모두 조회할 수 있을까? 이는 스프링에서 특별하게 제공하는 Map의 기능을 이용하면 된다. 

 

 

 

DiscountService

 

예를 들어 DiscountService라는 클래스가 있다고 하자. 이는 Map의 key값으로 String(DiscountPolicy의 이름이 될 것이다.), value값으로 DiscountPolicy인터페이스를 받고 있다.

 

이 클래스를 스프링 빈으로 등록하고 생성자 주입을 해주었다.

 

 

 

 

 

 

조금 알아보기 힘들지만, 위에 생성자에서 찍은 map과 list의 결과이다.

map에는 key로 클래스의 이름이 들어있고 value에는 해당 클래스의 객체참조값이 들어있다. DiscountPolicy 라는 빈은 RateDiscountPolicy와 FixDiscountPolicy라는 2개의 빈이 의존하고 있었기 때문에 자동 의존관계가 주입되어 이 2개가 map에는 key, value값으로 list에는 배열로서 나타내 진것을 확인할 수 있었다.

 

 

 

 

DicountService 의 Test

 

위의 DiscountService의 생성자 들이 자동주입이 될 수 있도록 AnnotaionConfigApplicationContext를 사용하여 스프링 빈을 등록할 때 DiscountService와 AutoAppconfig를 등록하였다. AutoAppConfig는 위에 예제들에서 사용한 모든 빈들을 ComponentScan해주는 설정파일이다. 물론 RateDiscountPolicy와 FixDiscountPolicy가 포함되어 있다. 

 

ac.getBean하여 빈을 받아온 후, discount라는 메서드를 먼저 TDD방식으로 만들었다. 이는 member와 price, disCountCode를 넘겨 어떤 할인 정책이냐에 따라 price가 얼마가 반환되는지 테스트하기 위한 메서드 이다.

 

2번의 테스트는 각각 rateDiscountPolicy와 fixDiscountPolicy를 넘겨서 해당 할인정책에 따라 가격이 잘 return이 되는지 확인하였다.

 

 

DicountService에 discount메서드를 추가함

 

discount메서드를 추가하였다. 

map에는 DiscountPolicy 빈을 의존하고 있는 RateDiscountPolicy와 FixDiscountPolicy라는 빈이 들어있기 때문에 get하여 해당 빈을 꺼내고 그 빈에 맞는 discount값을 return해주었다. 

 

 

 

테스트가 정상적으로 통과하였다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

'JAVA > Spring' 카테고리의 다른 글

빈 스코프  (0) 2021.12.23
빈 생명주기 콜백  (0) 2021.12.18
컴포넌트 스캔 (@ComponentScan)  (0) 2021.12.17
싱글톤 패턴과 스프링 컨테이너  (0) 2021.12.15
Spring Socket에 대해 알아보자! - 2. WebSocket 만들어보자  (0) 2020.06.23

@ComponentScan

 

- @ComponentScan은 @Component가 붙은 모든 클래스를 빈으로 등록해준다. 이 때 스프링 빈의 기본 이름은 클래스명을 사용하되 맨 앞글자는 소문자를 사용한다. @ComponentScan("service1") 이런식으로 빈의 이름을 설정해줄 수도 있다.

 

- 수동으로 빈을 등록할 때에는 빈 설정파일을 만들어서 @Configuration을 붙여주고 그 안에 @Bean을 만들어서 빈을 생성해었다. @ComponentScan은 설정파일을 굳이 만들지 않아도 자동으로 @Component 애노테이션이 붙은 클래스를 찾아서 빈으로 등록해주는 역할을 한다. 


- @ComponentScan은 스캔의 용도 뿐만 아니라 다음 애노테이션들이 있으면 부가적인 기능도 수행한다.


   1. @Contorller         : 스프링 MVC의 컨트롤러로 인식
   2. @Repository        : 스프링 데이터 접근 계층으로 인식하고, 데이터 계층의 스프링 예외로 변환해준다.
   3. @Configuration    : 스프링 설정 정보로 인식하고, 스프링 빈이 싱글톤을 유지하도록 추가처리를 한다. 

                                (SpringCGLB이용하여 싱글톤 로직)
   4. @Service            : 사실 @Service는 특별한 기능이 있지 않지만, 개발자들이 이 애노테이션을 보면

                                핵심 비즈니스 로직이 여기에 있겠구나 라고 비즈니스 계층을 인식하는데 도움을 준다.

 

 

 

@Autowired (의존관계 자동주입)


- 생성자에 @Autowired를 지정하면 스프링 컨테이너가 자동으로 해당 스프링 빈을 찾아서 주입해준다. 
- @Autowired는 타입기반으로 주입을 해주며 생성자에 파라메터가 많아도 다 알아서 찾아서 자동으로 주입해준다.

 

 

 

@ComponentScan 설명 다음에 @Autowired 설명을 한 이유가 있다.

스프링 컨테이너가 동작하는 순서대로 적은 것이다. 스프링 컨테이너는 처음에 생성이 되면 빈을 등록한다. @ComponentScan이 있으면 @Componen 애노테이션이 있는 클래스를 모두 찾아서 빈으로 등록하고, 그 빈들의 의존관계를 주입한다. 의존관계를 @Autowired로 자동주입하게 설정했으면 스프링은 @Autowired를 보고 빈들의 의존관계를 자동으로 설정해준다.

 

 

 

 

 

 

@ComponentScan의 Filter

 

 

- @ComponentScan은 includeFilters와 excludeFilters를 이용해서 스캔대상을 포함하거나 제외할 수 있다.

 

 

@ComponentScan의 Filter

 

 

- basePackages : 탐색할 패키지의 시작 위치를 설정한다. 이 패키지를 포함한 하위 패키지를 모두 탐색한다. 

- baseClasses: 탐색할 클래스의 시작 위치를 설정한다.

- 만약 아무 설정이 없으면 @ComponentScan이 붙은 클래스가 포함된 패키지가 시작위치가 된다.

 

- excludeFilters : ComponentScan에서 제외할 대상을 지정한다. 

- includeFilters : excludeFilters와 사용코드는 똑같다. ComponentScan의 대상을 추가로 지정한다.

 

 

권장하는 방법?

패키지 위치를 지정하지 않고, 설정 정보클래스의 위치를 프로젝트 최상단에 두는 것이다. 최근 스프링부트도 이 방법을 기본으로 제공한다.

 

 

 

 

싱글톤 패턴이란?

- 클래스의 인스턴스가 딱 1개만 생성되는 것을 보장하는 디자인 패턴이다.

 

싱글톤 패턴 클래스 예시

 

- 자바가 뜰 때,  SingletonPattern클래스의 static 영역에 new 가 있으니까 자기자신을 생성해서 instance변수에 넣어 놓는다.
- 조회할 때는 getInstance() 메소드를 사용해서 가져올 수 있다.
- private생성자를 이용해서 외부에서 new로 생성하는 것을 막아준다.

 

 

 

 

- 스프링 DI컨테이너는 위와 같은 싱글톤 방식으로 빈을 관리한다. 요청마다 새로운 객체를 생성하는 것이 아니라 이미 만들어진 객체를 공유해서 효율적으로 재사용할 수 있다. 

 

스프링 DI 컨테이너의 Bean 관리

 

 

 

 

 

싱글톤 방식의 주의점

-> 싱글톤 패턴이든, 스프링 같은 싱글톤 컨테이너를 사용하든, 객체 인스턴스를 하나만 생성해서 공유하는 싱글톤 방식은 여러 클라이언트가 하나의 같은 객체 인스턴스를 공유하기 때문에 싱글톤 객체는 상태를 유지(stateful)하게 설계하면 안된다. 무상태(stateless)로 설계해야 한다.

 


- 특정 클라이언트에 의존적인 필드가 있으면 안된다.
- 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안된다.
- 가급적 읽기만 가능해야 한다 (값을 수정하면 안된다)
- 필드 대신에 자바에서 공유되지 않는 지역변수, 파라미터, ThreadLocal등을 사용해야 한다.
- 스프링 빈의 필드에 공유값을 설정하면 정말 큰 장애가 발생할 수 있다..

 

 

 

직접 코드를 보면서 이해해보자.

 

Stateful 하게 설계된 클래스

 

이렇게 설계된 클래스가 있다고 가정해보자. order라는 메소드로 이름과 가격을 받는데 여기서 중요한건 private생성자로 int형 price가 선언되어 있음으로 인해서 상태를 유지할 수 있는 필드가 선언되어 있다는 것이다.

 

이 서비스 클래스의 order메소드를 사용해보자

 

아래 static class TestConfig는 빈으로 등록해주기 위해서 그냥 테스트용으로 간단하게 선언한 것이다.

 

StatefulServiceSingleton()의 중간에 보면 분명히 우리는 getBean을 사용하여 statefulService1, statefulService2를 각각 선언해서 member1과 member2가 서로 다른 가격을 주문한것으로 테스트를 하였다. 그런데 결과는...?

 

price1과 price2의 결과

결과는 price1과 price2가 모두 20000원이 나온다.

 

이유는.. statefulService1과 statefulService2는 같은 객체이기 때문이다. 맨윗줄에서 AnnotationConfigApplicationContext(TestConfig.class)를 해줌으로써 스프링 컨테이너가 만들어진다. 스프링 컨테이너는 Bean을 디폴트로 싱글톤객체로 관리하기 때문에 statefulService1과 statefulService2는 같은 객체이다.

 

같은 객체를 2번 가져다가 쓰니까 member2를 order할때 중간에 끼어드는 형태가 되어버린 것이고 결론적으로 price1과 price2는 20000으로 같게 나와버렸다.

 

이 문제를 해결하기 위해서 위에 설명한대로 우리는 스프링 빈을 무상태(stateless)로 설계를 해야한다. 즉, 특정 클라이언트에 의존적인 필드가 있게 설계하면 안된다는 뜻이다.

 

StatefulService클래스를 고쳐보자.

 

수정된 StatefulService

 

order 메소드를 int형으로 수정하고 상태를 유지하는 필드를 사용하는 것이 아니라 메소드 안에서 바로 return을 해주는 식으로 변경하였다. price를 얻기 위해 만들었던 getPrice메소드도 삭제하였다.

 

 

테스트 코드도 이렇게 바꾸어서 찍어보면 우리가 원하는대로 price1과 price2는 각각 member1과 member2의 가격이 나온다.

 

 

 

 

 

 

 


@Configuration 과 @Bean


-> @Bean만 사용해도 스프링 빈으로 등록이 되기는 하지만, 싱글톤이 보장이 되지 않는다. (의존관계 주입이 필요해서 메서드를 직접 호출할 때 싱글톤이 보장이 되지 않는다는 의미)

 

 

-> @Bean으로 등록한 빈은 자바코드로 return new 구현클래스(); 와 같은 방식으로 되어 있어서 여러번 이 객체를 사용하면 매번 다른 객체가 호출되어야 할 것 같지만 이상하게도 싱글톤을 보장한다.. 왜?!

 

 

-> @Configuration과 @Bean을 사용하여 스프링빈을 등록하면 (예를 들어 AppConfig 클래스가 스프링빈으로 등록되었다고 한다면,) 스프링 컨테이너에는 바로 내가만든 AppConfig를 등록하는 것이 아니라 스프링이 CGLIB라는 바이트코드 조작 라이브러리를 사용해서 AppConfig를 상속받은 임의의 다른 클래스를 만들어버리고 그 다른 클래스를 빈으로 등록한다.
(ex. AppConfig$$CGLIB$$bd23232)

 

 

-> AppConfig@CGLIB는 호출한 빈이 스프링 컨테이너에 등록이 되어있으면 스프링컨테이너에서 찾아서 반환하고 그렇지 않으면 기존 로직을 호출해서 빈을 생성하고 컨테이너에 등록 하는 과정이 코드로 정의가 되어있을 것이다. 이러한 과정에서 등록되어있는 빈은 새로 만드는 것이 아니라 등록되어있는 것을 찾기 때문에 싱글톤이 보장이 되는 것이다.

 

1. SRP : 단일 책임 원칙
-> 한 클래스는 하나의 책임만 가져야 한다.
-> 가장 중요한건 책임의 범위이다. 너무 작게하면 너무 잘게 쪼개지고 너무 크게하면 책임이 많아지기 때문에 적절하게 조절해야 한다.
-> 변경이 있을 때 파급효과가 적으면 단일 책임 원칙을 잘 따른 것.

2. OCP : 개방-폐쇄 원칙
-> 확장에는 열려 있으나 변경에는 닫혀 있어야 한다.
-> 인터페이스를 구현한 새로운 클래스를 하나 더 만들어서 새로운 기능을 구현

3. LSP : 리스코프 치환 원칙
-> 프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야한다.
-> 다형성에서 하위 클래스는 인터페이스 규약을 다 지켜야 한다는 것.
-> 예를 들어서 자동차 인터페이스의 '엑셀'은 앞으로 가라는 기능인데 뒤로 가게 구현을 했다면 LSP 위반임.
 
4. ISP : 인터페이스 분리 원칙
-> 특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다.
-> 자동차 인터페이스를 운전 인터페이스, 정비 인터페이스로 분리한다. 
-> 그러면 사용자 클라이언트를 운전자 클라이언트, 정비사 클라이언트로 분리할 수 있다. 정비 인터페이스 자체가 변해도 운전자 클라이언트에 영향을 주지 않는다.

5. DIP : 의존관계 역전 원칙
-> 프로그래머는 "추상화에 의존해야지 구체화에 의존하면 안된다"
-> 구현클래스에 의존하지 말고, 인터페이스에 의존하라는 뜻

'JAVA > JAVA' 카테고리의 다른 글

Java 로깅 프레임워크(Log4j, Logback, Log4j2)  (0) 2022.04.19

+ Recent posts