[스프링부트핵심가이드] 06. 데이터베이스 연동

오늘내일·2023년 11월 5일
0

책 리뷰

목록 보기
5/11

6.2 ORM(Object Relational Mapping)

ORM은 객체와 관계형 데이터베이스의 테이블을 자동으로 매핑하는 방법이다. ORM을 통해서 쿼리문 작성이 아닌 코드로 데이터를 조작할 수 있다.

  • ORM 장점
    - ORM을 사용하여 쿼리를 객체지향적으로 조작할 수 있다.
    • 재사용 및 유지보수가 편리하다.
    • 데이터베이스에 대한 종속성이 줄어든다.
  • ORM 단점
    - ORM만으로 온전한 서비스를 구현하기에 한계가 있다.(복잡한 쿼리가 필요할 때가 있다.)
    • 애플리케이션의 객체 관점과 데이터베이스의 관계 관점의 불일치가 발생한다.

6.3 JPA(Java Persistence API)

JPA는 자바 진영의 ORM 기술 표준으로 채택된 인터페이스의 모음이다. JPA의 매커니즘을 보면 내부적으로 JDBC를 사용한다. JPA 구현체는 Hibernate, EclipseLink, DataNucleus 등이 있다.

JPA는 자바에 비유하면 인터페이스이고, JPA구현체는 클래스에 해당한다.

6.4 하이버네이트

하이버네이트는 자바의 ORM 프레임워크이자, JPA 구현체 중 하나이다. 보통 하이버네이트의 기능을 더욱 편하게 사용할 수 있도록 모듈화한 Spring Data JPA를 활용한다.

6.4.1 Spring Data JPA
Spring Data JPA를 통해 하이버네이트의 엔티티 매니저(Entity Manager)를 직접 다루지 않고, 리포지토리를 정의해 사용함으로써 스프링이 적합한 쿼리를 동적으로 생성하는 방식으로 데이터베이스를 조작한다.

6.5 영속성 컨텍스트(Persistence Context)

영속성 컨텍스트(Persistence Context)는 애플리케이션과 데이터베이스 사이에서 엔티티와 레코드의 괴리를 해소하는 기능과 객체를 보관하는 기능을 수행한다. 영속성 컨텍스트는 세션 단위의 생명주기를 가진다. 영속성 컨텍스트에 들어온 객체를 영속 객체(Persistence Object)라고 한다. 엔티티 매니저는 영속성 컨텍스트에 접근하기 위한 수단으로 사용된다.

(세션은 보통 일정시간 동안 클라이언트의 상태 정보를 유지하고 관리하는 데 사용된다. ex : 사용자 인증 정보 유지, 장바구니 유지, 사용자 활동 추적, 상태관리 등)

  • 영속성 컨텍스트의 특징
    - 1차 캐시 : 영속성 컨텍스트는 엔티티의 인스턴스를 관리하는 캐시를 가지고 있어 데이터베이스의 반복적인 조회를 방지한다.
    • 지연 로딩(Lazy Loading) : 영속성 컨텍스트는 연관된 엔티티를 필요할 때까지 로드하지 않고, 필요한 시점에 데이터베이스에서 가져온다.
    • 변경 감지(Dirty Checking) : 영속성 컨텍스트는 엔티티의 상태를 주기적으로 확인하여 변경된 내용이 있는지 여부를 감지한다.
    • 트랜잭션 지원 : 영속성 컨텍스트는 트랜잭션을 지원하고 트랜잭션 내에서 엔티티의 변경 내용을 추적하고, 트랜잭션이 커밋될 때 변경 사항을 데이터베이스에 반영한다.

6.5.1 엔티티 매니저(Entity Manager)
엔티티 매니저는 엔티티 매니저 팩토리(Entity ManagerFactory)에서 만들어지는 엔티티를 관리하는 객체로, 데이터베이스에 접근해서 CRUD 작업을 수행한다.

Spring Data JPA를 사용하면 리포지토리를 사용해서 데이터베이스에 접근하는데, 내부 구현체에서 엔티티 매니저를 사용한다. 그리고 스프링 부트에서는 자동 설정 기능이 있기 때문에 application.properties에서 작성한 최소한의 설정으로 엔티티 매니저 팩토리가 동작한다.

반면 하이버네이트에서는 엔티티 매니저 팩토리 객체를 사용하기 위해 persistence.xml이라는 설정 파일을 구성하고 사용할 수 있다.

6.5.2 엔티티의 생명주기
비영속(new), 영속(managed), 준영속(detached, 영속성 컨텍스트에 의해 관리되던 엔티티 객체가 컨텍스트와 분리된 상태), 삭제(removed)

6.6 데이터베이스 연동

6.6.1 프로젝트 생성
Spring Data Jpa 의존성을 추가해 준다. gradle기준으로 db로 mysql을 사용할 경우 보통 아래와 같이 추가한다.

implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'com.mysql:mysql-connector-j'

그리고 데이터베이스 정보를 application.properties에 작성해 준다.

// 1. 데이터베이스의 드라이버 정의
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
// 2. 해당 db의 경로를 지정(여기서는 zeroorder라는 테이블 경로까지 지정)
spring.datasource.url=jdbc:mysql://localhost:3306/zeroorder?useSSL=false&useUnicode=true&allowPublicKeyRetrieval=true
// 3. DB접속 아이디 입력
spring.datasource.username=root
// 4. DB접속 암호 입력
spring.datasource.password={본인 패스워드}

// 5. 하이버네이트가 생성한 쿼리문 출력 옵션
spring.jpa.show-sql=true
// 6. 데이터베이스 자동으로 조작하는 옵션
spring.jpa.hibernate.ddl-auto=create
  1. db 경로를 지정할 때 쿼리 파라미터를 통해서 useSSL(DB에 SSL로 연결할건지 여부), allowPublicKeyRetrieval(서버에서 RSA 공개키를 검색하거나 가져와야하는지 여부) 등 설정을 할 수 있다. 보통 useSSL=false로 지정하면 보안을 위해 allowPublicKeyRetrieval=true로 해서 RSA 공개키를 가져오게 하는게 좋다.
    RSA : 공개키 암호화 시스템

  2. 하이버네이트가 생성한 쿼리문 출력 옵션을 true로 해서 출력할 경우 'spring.jpa.properties.hibernate.format_sql=true'를 추가 입력하여 보기 좋게 포맷팅 할 수 있다.

  3. 자동조작 옵션은 다음과 같이 있다. 주로 운영환경에서는 기존 데이터의 보호를 위해 validate, none을 주로 사용하고, 개발환경에서는 create 또는 update를 주로 사용한다.

    • create : 애플리케이션이 가동되고 SessionFactory가 실행될 때 기존 테이블을 지우고 새로 생성한다.
    • create-update : create와 동일한 기능을 수행하나 애플리케이션을 종료하는 시점에 테이블을 지운다.
    • update : SessionFactory가 실행될 때 객체를 검사해서 변경된 스키마(데이터베이스의 구조와 제약조건에 관한 전반적인 명세를 기술한 메타데이터의 집합)를 갱신한다. 기존에 저장된 데이터는 유지된다.
    • validate : update처럼 객체를 검사하지만 스키마는 건드리지 않는다. 검사 과정에서 데이터베이스의 테이블 정보와 객체의 정보가 다르면 에러가 발생한다.
    • none : ddl-auto기능 사용 하지 않음

6.7 엔티티 설계

JPA에서 엔티티는 데이터베이스의 테이블에 대응하는 클래스이다. 따라서 엔티티에 어노테이션을 사용하여 테이블 간의 연관관계를 정의한다.

6.7.1 엔티티 관련 기본 어노테이션

  • @Entity
    - 해당 클래스가 엔티티임을 명시하는 어노테이션이다. 클래스와 테이블과 1:1로 매칭되며, 클래스의 인스턴스는 DB의 한 레코드를 의미한다.
  • @Table
    - 클래스와 테이블 명을 다르게 하고 싶을 경우 사용한다.(ex : @Table(name = 값))
  • @Id
    - 테이블 기본값 역할을 한다. 모든 엔티티는 @Id 어노테이션이 필요하다.
  • @GeneratedValue(strategy = 값)
    - @Id와 함께 사용되며, id를 어떻게 자동생성할 지 결정할 때 사용한다.
    • auto, identity, sequence, table 등의 옵션이 있다.
  • @Column
    - 엔티티 클래스의 필드는 자동으로 테이블 칼럼으로 매핑된다. 아래의 속성을 사용해서 옵션을 줄 수 있다.
    • name : 컬럼명을 설정한다.
    • nullable : null처리 가능 여부를 명시한다.
    • length : 데이터 최대 길이 지정한다.
    • unique : 해당 컬럼을 유니크로 설정한다.
  • @Transient
    - 엔티티 클래스에 선언되어 있는 필드지만 데이터베이스에 필요 없을 경우 사용한다.

6.8 리포지터리 인터페이스 설계

6.8.1 리포지터리 인터페이스 생성
리포지토리는 Spring Data JPA가 제공하는 인터페이스이다.

public interface MemberRepository extends JpaRepository<Member, Long> {
}

위와 같이 JpaRepository를 상속받아 인터페이스를 만든다. <>에는 해당 엔티티와 아이디의 타입을 입력한다.

6.8.2 리포지터리 메서드의 생성 규칙

  • findBy
  • And, Or
  • Like/NotLike
  • StartsWith/StartingWith
  • EndsWith/EndingWith
  • IsNull/IsNotNull
  • True/False
  • Before/After
  • LessThan/GreaterThan
  • Between
  • OrderBy(끝에 Asc, Desc 설정 가능)
  • CountBy

6.9 DAO(Data Access Object) 설계

DAO는 데이터베이스에 접근하기 위한 로직을 관리하기 위한 객체이다. DAO는 스프링에서 리포지터리 클래스로 대체될 수도 있다. 규모가 큰 서비스에서는 데이터를 다루는 중간 계층을 두는 것이 유지보수 측면에서 용이한 경우가 많다.

6.9.1 DAO 클래스 생성
1. DAO 인터페이스를 생성한다.
2. 생성한 인터페이스를 구현하는 DAO 클래스를 만든다. 클래스를 만들 때 스프링이 관리하는 빈으로 등록하기 위해 @Service 또는 @Component 어노테이션을 지정한다.
3. 클래스 내 각 메서드를 구현한다.
리턴값을 DTO객체에 담아서 전달할 지 엔티티 객체로 전달할 지 견해 차이가 있다.

  • 메서드 생성 예시
// 1. DB에 저장
@Override
public Member insertMember(Member member){
	Member savedMember = memberRepository.save(member);
    return savedMember;
}

// 2. DB조회
@Override
public Member selectMember(Long id){
	Member selectedMember = memberRepository.getById(id);
    return selectedMember;
}

// 3. DB업데이트
@Override
public Member updateMemberName(Long id, String name) throws Exception{
	Optional<Member> selectedMember = memberRepository.findById(id);

    Member updatedMember;
    if (selectedMember.isPresent()) {
        Member member = selectedMember.get();
        
        member.setName(name);
        member.setUpdatedAt(LocalDateTime.now());
        
        updatedMember = memberRepository.save(member);
    } else {
    	throw new Exception();
    }
    
    return updatedMember;
}

// 4. DB 데이터 삭제
@Override
public Member deleteMember(Long id) throws Exception{
	Optional<Member> selectedMember = memberRepository.findById(id);

    if (selectedMember.isPresent()) {
        Member member = selectedMember.get();
        
        memberRepository.delete(member);
    } else {
    	throw new Exception();
    }
}

데이터를 조회할 때 getById() 또는 findById()를 사용할 수 있는데, 각 차이는 다음과 같다.

  • getById() : 내부적으로 EntityManager의 getReference() 메서드를 호출한다. getReference() 메서드를 호출하면 프락시* 객체를 리턴한다. 따라서 실제로 사용하기 전까지는 DB에 접근하지 않으며, 만약 실제로 사용할 때 프록시에서 DB에 접근하려고 할 때 데이터가 없다면 EntityNotFoundException을 발생시킨다.
  • findById() : 영속성 컨텍스트의 캐시에서 값을 조회한 후 영속성 컨텍스트에 값이 존재하지 않으면 실제 데이터베이스에서 데이터를 조회한다. 리턴값으로 Optional** 객체를 반환한다.
  • *프락시 객체 : 프락시 객체는 다른 객체를 감싸거나 래핑하여 해당 객체에 대한 접근을 제어하거나 추가적인 기능을 제공하는데 사용되는 디자인 패턴이다.(getById()의 경우 프락시 객체는 DB조회 시점을 늦춘다. 그 밖에 보안, 로깅 등에 사용된다.)
  • **Optional객체 : Optional 객체는 null값일 수도 있는 어떤 변수를 감싸주는 '래퍼(Wrapper)' 객체이다. Optional 클래스는 제네릭으로 값의 타입을 지정해줘야 한다. Optional 클래스는 null 일 수도 있는 값을 다루기 위한 다양한 메서드들을 제공한다.(ex : isPresent(), orElseThrow, get(), orElse(), filter(), map() 등)

그리고 데이터를 업데이트, 삭제할 경우 findById를 통해 객체를 불러온 후 저장 또는 삭제한다.

6.10 DAO 연동을 위한 컨트롤러와 서비스 설계

6.10.1 서비스 클래스 만들기
기능이 복잡할 경우 서비스 레이어와 비즈니스 레이어로 나누기도 한다. 나누어 구현할 경우 도메인을 활용한 세부 기능들은 비즈니스 레이어에서 구현하고, 핵심 기능을 전달하도록 서비스 레이어에 구현한다.

  1. 서비스 인터페이스를 생성한다.
  2. 인터페이스를 구현하는 클래스를 생성한다. 클래스 생성 시 필요한 DAO 객체를 주입 받는다.
  3. 각 메서드를 구현한다.

각 메서드의 반환값은 보통 dto클래스를 통해 데이터를 전송한다. 이 때 입력받는 값의 dto클래스, 반환하는 값의 dto클래스를 각각 따로 만들수 있다. dto 클래스를 구현할 때 빌더 메서드를 추가로 구현할 수도 있다. 빌더 메서드는 데이터 클래스를 사용할 때 생성자로 초기화할 경우 모든 필드에 값을 넣거나 null을 명시적으로 사용해야 하는데 이러한 단점을 보완하여 필요한 데이터만 설정할 수 있도록 한다.

6.11 롬복

반복되는 코드를 줄이기 위해 롬복 라이브러리를 많이 사용한다. 아래와 같은 어노테이션이 주로 사용된다.

@Getter / @Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
@RequiredArgsConstructor : 필드 중 final이나 @NotNull이 설정된 변수를 매개변수로 갖는 생성자를 자동 생성한다.
@EqualsAndHashCode

profile
다시 시작합니다.

0개의 댓글