DI를 사용하기 위해 객체 생성이 우선되어야 함
하지만 언제, 어디서, 누가 객체를 생성할까?
-> Spring 프레임워크가 필요한 객체를 생성하고 관리하는 역할을 대신 해줌
Spring IoC 컨테이너
: 'Bean'을 모아둔 컨테이너
Bean
: Spring이 관리하는 객체
Spring 'Bean' 등록 방법
@Component: 'Bean'으로 등록하고자하는 클래스 위에 설정public class MemoService -> memoService로 저장됨@ComponentScan: Spring 서버가 뜰 때 @ComponentScan에 설정해 준 packages 위치와 하위 packages들을 전부 확인하여 @Component 가 설정된 클래스들을 'Bean'으로 등록해줌@SpringBootApplication에 의해 default 설정 되어있음Spring 'Bean' 사용 방법
@Autowired @Component
public class MemoService {
@Autowired
private MemoRepository memoRepository;
// ...
}
Spring에서 IoC 컨테이너에 저장된 memoRepository 'Bean'을 해당 필드에 DI 즉, 의존성 주입 해줌
@Component
public class MemoService {
private final MemoRepository memoRepository;
@Autowired
public MemoService(MemoRepository memoRepository) {
this.memoRepository = memoRepository;
}
// ...
}
```
객체의 불변성을 확보할 수 있기 떄문에 일반적으로 생성자를 사용하여 DI하는 것이 좋음
@AutoWired 적용 조건
Spring IoC 컨테이너에 의해 관리되는 'Bean' 객체만 DI에 사용될 수 있음
@AutoWired 생략 조건
Spring 4.3 부터, 생성자 선언이 1개 일 때만 생략 가능
Lombok의 @RequiredArgsConstructor를 사용하면 다음과 같이 코딩 가능
@Component
@RequiredArgsConstructor // final로 선언된 멤버 변수를 파라미터로 사용하여 생성자를 자동으로 생성합니다.
public class MemoService {
private final MemoRepository memoRepository;
// public MemoService(MemoRepository memoRepository) {
// this.memoRepository = memoRepository;
// }
...
}
```
ApplicationContext
3 Layer Annotation
- Spring 3 Layer Annotation은 Controller, Service, Repository의 역할로 구분된 클래스들을 'Bean'으로 등록할 때 해당 'Bean' 클래스의 역할을 명시하기위해 사용됨
@Controller,@RestController,@Service,@Repository- Spring 3 Layer Annotaiton은 모두
@Component가 추가돼있기 때문에,@Component가 아닌 3 Layer Annotation을 사용해 'Bean'으로 등록
public class Memo {
private Long id;
private String username;
private String contents;
}
ORM (Object-Relational Mapping)
: 객체와 DB의 관계를 매핑해주는 도구
- Object: 객체 지향 언어 (자바, 파이썬)
- Relational: 관계형 데이터베이스 (H2, MySQL)
JPA (Java Persistence API)
: 자바 ORM 기술에 대한 표준 명세
- 애플리케이션과 JDBC 사이에서 동작되고 있음
- JPA를 사용하면 DB 연결 과정을 직접 개발하지 않아도 자동으로 처리해줌
- 객체를 통해 간접적으로 DB 데이터를 다룰 수 있기 때문에 매우 쉽게 DB 작업을 처리할 수 있음
Hibernate
- JPA는 표준 명세이고, 이를 실제 구현한 프레임워크 중 사실상 표준이 Hibernate
- SpringBoot에서는 기본적으로 하이버네이트 구현체를 사용 중
Entity
: JPA에서 관리되는 클래스 = 즉, 객체를 의미함
- Entity 클래스는 DB의 테이블과 매핑되어 JPA에 의해 관리됨
@Entity // JPA가 관리할 수 있는 Entity 클래스 지정
@Table(name = "memo") // 매핑할 테이블의 이름을 지정
public class Memo {
@Id
private Long id;
// nullable: null 허용 여부
// unique: 중복 허용 여부 (false 일때 중복 허용)
@Column(name = "username", nullable = false, unique = true)
private String username;
// length: 컬럼 길이 지정
@Column(name = "contents", nullable = false, length = 500)
private String contents;
}
@Entity: JPA가 관리할 수 있는 Entity 클래스로 지정할 수 있음@Entity(name="Memo"): Entity 클래스 이름을 지정할 수 있음 (default: 클래스명)@Table: 매핑할 테이블을 지정해줌@Table(name="memo"): 매핑할 테이블의 이름을 지정할 수 있음 (default: Entity명)@Column(name="username", nullable=false, unique=true): 필드와 매핑할 테이블의 컬럼을 지정할 수 있음 (default: 객체의 필드명)@Id: 테이블의 기본 키를 지정해줌@Id 옵션만 설정하면 기본 키 값을 개발자가 직접 확인하고 넣어줘야 하는 불편함이 생김@GeneratedValue 옵션을 추가하면 기본 키 생성을 DB에 위임할 수 있음@GeneratedValue(strategy = GenerationType.IDENTITY)EntityManager
: Entity를 관리하는 관리자
- 영속성 컨텍스트에 접근하여 Entity 객체들을 조작하기 위해서는 EntityManager가 필요함
- 개발자들은 EntityManager를 사용해 Entity를 저장하고 조회하고 수정하고 삭제할 수 있음
- EntityManager는 EntityManagerFactory를 통해 생성하여 사용할 수 있음
EntityManagerFactory
: 일반적으로 DB 하나에 하나만 생성되어 애플리케이션이 동작하는 동안 사용됨- EntityManagerFactory를 만들기 위해서는 DB에 대한 정보를 전달해야함
EntityManagerFactory emf = Persistence.createEntityManagerFactory("memo");
- 해당 코드를 호출하면 JPA는 persistence.xml 의 정보를 토대로 EntityManagerFactory를 생성합니다.
EntityManager em = emf.createEntityManager();- 코드를 호출하면 EntityManagerFactory를 사용하여 EntityManager를 생성할 수 있습니다.
트랜잭션
: DB 데이터들의 무결성과 정합성을 유지하기 위한 하나의 논리적 개념
- DB의 데이터들을 안전하게 관리하기 위해 생겨난 개념
- 여러 개의 SQL이 하나의 트랜잭션에 포함될 수 있음
- 이때, 모든 SQL이 성공적으로 수행이 되면 DB에 영구적으로 변경을 반영하지만 SQL 중 단 하나라도 실패한다면 모든 변경을 되돌림
EntityTransaction et = em.getTransaction();et.begin();: 트랜잭션을 시작하는 명령어입니다.et.commit();: 트랜잭션의 작업들을 영구적으로 DB에 반영하는 명령어입니다.et.rollback();: 오류가 발생했을 때 트랜잭션의 작업을 모두 취소하고, 이전 상태로 되돌리는 명령어입니다.
@Id로 매핑한 기본 키 즉, 식별자 값을 저장
em.persist(memo) 메서드가 호출되면 memo Entity 객체를 캐시 저장소에 저장함

em.find(Memo.class, 1);em.find(Memo.class, 1); 호출 시 캐시 저장소를 확인 한 후 해당 값이 없다면? -> DB SELECT 조회 후 해당 값을 캐시 저장소에 저장하고 반환함
em.find(Memo.class, 1); 호출 시 캐시 저장소에 식별자 값이 1이면서 Memo Entity 타입인 값이 있는지 조회함

em.remove(entity);
JPA는 트랜잭션처럼 SQL을 모아서 한번에 DB에 반영하는 것을 구현하기 위해 쓰기 지연 저장소를 만들어 SQL을 모아두고 있다가 트랜잭션 commit 후 한번에 DB에 반영함
flush()
em.flush(); 메서드의 호출임Insert, Update, Delete 즉, 데이터 변경 SQL을 DB에 요청 및 반영하기 위해서는 트랜잭션이 필요함 (조회 제외)

em.flush();가 호출되면 Entity의 현재 상태와 저장한 최초 상태를 비교함(flush와 commit 실행순서)
1. flush 발생 (SQL은 실행되지만, 아직 rollback 가능한 상태, DB에 반영은 됨(임시 상태) -> commit 할 때 자동으로 호출됨)
-> update SQL 발생
-> DB에 보냄
2. commit
-> 트랜잭션 확정

비영속 (Transient)
Memo memo = new Memo();
영속 (Managed)
em.persist(memo)
준영속 (Detached)
영속성 컨텍스트에 저장되어 관리되다가 분리된 상태를 의미함
영속 상태에서 준영속 상태로 바꾸는 방법
em.detach(memo);em.contains(memo);는 해당 객체가 영속성 컨텍스트에 저장되어 관리되는 상태인지 확인하는 메서드em.clear();em.close();준영속 상태에서 다시 영속 상태로 바꾸는 방법
em.merge(memo);em.contains(memo)에서 비영속 상태의 memo는 merge() 호출 후에 해당 memo 객체가 영속성 컨텍스트에 저장된게 아니라, mergedMemo로 새롭게 생성되어 영속성 컨텍스트에 저장되었기 때문에 false가 반환됨삭제 (Removed)
em.remove(memo);
remove(entity): 삭제하기 위해 조회해온 영속 상태의 Entity를 파라미터로 전달받아 삭제 상태로 전환함
(.find(): 캐시 저장소에 없다면, DB 조회 결과를 바로 1차 캐시에 넣음, 그리고 다음부터는 캐시에서 꺼냄)
// JPA 설정
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.show_sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.use_sql_comments=true
@PersistenceConext 어노테이션을 사용해 자동으로 생성된 EntityManager를 주입받아 사용할 수 있음@Transactional 어노테이션을 클래스나 메서드에 추가하면 쉽게 트랜잭션 개념을 적용할 수 있음@Transactional은 해당 클래스 내부의 모든 메서드에 트랜잭션 기능을 부여함@Transactional 어노테이션이 추가되어있기 때문에 readOnly = true 옵션인 @Transactional을 덮어쓰게 되어 readOnly = false 옵션으로 적용됨 (조회만 하면 안되고, 변경을 해야하기 때문)@TransactionalreadOnly = true 옵션이 설정된 @Transactional을 적용하면 좋음
Spring은 어떻게 Service부터 Repository까지 Transaction을 유지할 수 있는 걸까
@Transactional에서 트랜잭션 전파 옵션을 지정할 수 있음@Transactional(propagation = Propagation.REQUIRED)
Spring Data JPA
: JPA를 쉽게 사용할 수 있게 만들어놓은 모듈
- JPA를 추상화시킨 Repository 인터페이스를 제공함
- Repository 인터페이스는 Hibernate와 같은 JPA 구현체를 사용해 구현한 클래스를 통해 사용됨
public interface MemoRepository extends JpaRepository<Memo, Long> {
}
JpaRepository<"@Entity 클래스", "@Id의 데이터 타입">를 상속받는 interface로 선언함@Entity 클래스 위치에 Memo Entity를 추가했기 때문에 해당 MemoRepository는 DB의 memo 테이블과 연결되어 CRUD 작업을 처리하는 인터페이스가 됨public MemoResponseDto createMemo(MemoRequestDto requestDto) {
// RequestDto -> Entity
Memo memo = new Memo(requestDto);
// DB 저장
Memo saveMemo = memoRepository.save(memo);
// Entity -> ResponseDto
MemoResponseDto memoResponseDto = new MemoResponseDto(saveMemo);
return memoResponseDto;
}
SimpleJpaRepository의 save 메서드를 확인해보면 영속성 컨텍스트에 entity를 저장하는 코드가 작성되어있음

save 메서드를 사용해 데이터를 저장할 수 있음
@Transactional이 적용되어있는 것을 확인할 수 있음findAll
public List<MemoResponseDto> getMemos() {
// DB 조회
return memoRepository.findAll().stream().map(MemoResponseDto::new).toList();
}
반환을 List 자료형으로 받기 때문에 변환해서 반환해줘야함
findAll 메서드를 사용해 해당 테이블의 전체 데이터를 조회할 수 있음

findById
private Memo findMemo(Long id) {
return memoRepository.findById(id).orElseThrow(() ->
new IllegalArgumentException("선택한 메모는 존재하지 않습니다.")
);
}
SimpleJpaRepository의 findById 메서드를 확인해보면, 반환 타입이 Optional인 것을 확인할 수 있음

Optional<Entity 타입>을 반환 타입으로 받고 추가적으로 null을 체크하거나
위 코드와 같이 orElseThrow를 사용해 반환 값이 null일 경우 예외를 던지도록 처리할 수 있음
update
@Transactional
public Long updateMemo(Long id, MemoRequestDto requestDto) {
// 해당 메모가 DB에 존재하는지 확인
Memo memo = findMemo(id);
// memo 내용 수정
memo.update(requestDto);
return id;
}
SimpleJpaRepository에 update라는 메서드는 존재하지 않음
따라서, 위 코드처럼 영속성 컨텍스트의 변경감지를 통해 update를 진행해야함
변경감지가 적용되기 위해 해당 메서드에 @Transactional을 추가함
delete
public Long deleteMemo(Long id) {
// 해당 메모가 DB에 존재하는지 확인
Memo memo = findMemo(id);
// memo 삭제
memoRepository.delete(memo);
return id;
}

@Transactional이 적용되어있는 것을 확인할 수 있음