Showing

[스프링부트] Spring Data JPA 적용(1) Entity 클래스 작성, 빌더패턴 vs 생성자 패턴 본문

JAVA, SPRING/스프링 부트와 AWS로 혼자 구현하는 웹서비스

[스프링부트] Spring Data JPA 적용(1) Entity 클래스 작성, 빌더패턴 vs 생성자 패턴

RabbitCode 2023. 6. 1. 18:02

*이동욱 저, 스프링부트와 aws로 혼자 구현하는 웹서비스를 학습하면서 작성한 포스팅입니다.

 

1.  build.gradle에 의존성 등록

    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'com.h2database:h2'

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'com.h2database:h2'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

spring-boot-starter-data-jpa : 스프링 부트용 Spring Data JPA 추상화 라이브러리, 스프링 부트 버전에 맞춰 자동으로 JPA 관련 라이브러리들의 버전을 관리해준다.

h2 : 인메모리 관계형 데이터베이스, 별도의 설치 필요없이 프로젝트 의존성만으로 관리할 수 있다. 메모리에서 실행되기 때문에 애플리케이션을 재시작할 떄마다 초기화된다는 점을 이용하여 테스트 용도로 많이 사용됨, 이 책에서는 JPA 테스트, 로컬 환경에서의 구동에서 사용할 예정

2. 본격적인 JPA 기능 사용

위와 같이 domain 패키지를 만든다

domain 패키지는 도메인을 담을 패키지이다. 여기서 도메인이란 게시글, 댓글, 회원, 정산, 결제 등 소프트웨어에 대한 요구사항 혹은 문제 영역이라고 생각하면 된다. xml에 쿼리를 담고, 클래스는 오로지 쿼리의 결과만 담던 일들이 모두 도메인 클래스라고 불리는 곳에서 해결된다. 

domain 패키지에 posts 패키지와 Posts 클래스를 만든다.

(1) Posts 클래스 코드

package com.jojo.book.springbootwebservice.domain.posts;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@Getter
@NoArgsConstructor
@Entity
public class Posts {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(length = 500, nullable = false)
    private String title;

    @Column(columnDefinition = "TEXT", nullable = false)
    private String content;
    private String author;

    @Builder
    public Posts(String title, String content, String author) {
        this.title = title;
        this.content = content;
        this.author = author;
    }
}

Posts 클래스는 실제 DB의 테이블과 매칭될 클래스이며 보통 Entity 클래스라고도 한다. JPA를 사용하면 DB 데이터에 작업할 경우 실제 쿼리를 날리기보다는, 이 Entity 클래스의 수정을 통해 작업한다. 

이 Posts 클래스에는 한 가지 특이점이 있다. 바로 Setter 메소드가 없다는 점이다. 자바빈 규약으로 인해 getter/setter를 무작정 생성하는 경우가 있는데 이러면 해당 클래스의 인스턴스 값들이 언제 어디서 변해야 하는지 코드상 구분하기가 어려워 차부 기능 변경시 정말 복잡해진다.

그래서 Entity 클래스에서는 절대 Setter 메소드를 만들지 않는다. 대신, 해당 필드의 값 변경이 필요하면 정확히 그 목적과 의도를 나타낼 수 있는 메소드를 추가해야만 한다. 

 


잘못된 사용 예

public class Order{
    public void setStatus(boolean status){
	     this.status = status
    }
}

public void 주문서비스의_취소이벤트(){
    order.setStatus(false);
}

올바른 사용 예

public class Order{
    public void cancelOrder(){
	     this.status = false;
    }
}

public void 주문서비스의_취소이벤트(){
    order.cancelOrder();
}

 

 


 

코드 설명

package com.jojo.book.springbootwebservice.domain.posts;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@Getter // <6>
@NoArgsConstructor // <5>
@Entity // <1>
public class Posts {
    @Id // <2>
    @GeneratedValue(strategy = GenerationType.IDENTITY) // <3> : auto_increment로 만들기 위함
    private Long id;

    @Column(length = 500, nullable = false) // <4> : 괄호 안에서 기본 값 변경
    private String title;

    @Column(columnDefinition = "TEXT", nullable = false)
    private String content;
    private String author;

    @Builder // <7>
    public Posts(String title, String content, String author) {
        this.title = title;
        this.content = content;
        this.author = author;
    }
}

어노테이션 순서는 주요 어노테이션을 클래스에 가깝게 두는 스타일이 있다.

@Entity는 JPA의 어노테이션이며, @Getter와 @NoArgsConstructor는 롬복의 어노테이션이다. 롬복은 코드를 단순화시켜 주지만 필수 어노테이션은 아니다. 이렇게 하면 이후에 코틀린 등의 새 언어 전환으로 롬복이 더이상 필요없을 경우 쉽게 삭제할 수 있다.

(2) JPA에서 제공하는 어노테이션

_1 @Entity : 테이블과 링크될 클래스임을 나타냄, 기본값으로 클래스의 카멜케이스 이름을 언더스코어 네이밍으로 테이블 이름을 매칭
_2 @Id : 해당 테이블의 PK 필드를 나타냄

_3 @GeneratedValue : PK 생성 규칙을 나타냄. 스프링 부트 2.0에서는 GenerationType.IDENTITY 옵션을 추가해야만 auto_increment가 된다.

_4 @Column : 테이블의 칼럼을 나타내며 사실은 굳이 선언하지 않더라도 해당 클래스의 필드는 모두 칼럼이 된다. 사용하는 이유는, 기본값 외에 추가로 변경이 필요한 옵션이 있으면 사용한다. 문자열의 경우 VARCHAR(255)가 기본값인데, 사이즈를 500으로 늘리고 싶거나 타입을 TEXT로 변경하고 싶은 등의 경우에 사용한다.

 

 ***웬만하면 Entity의 PK는 Long 타입의 Auto Increment를 추천한다.(MySQL 기준으로 이렇게 하면 bigint 타입이 된다) 주민등록번호와 같이 비즈니스상 유니크 키나, 여러 키를 조합한 복합키로 PK를 잡을 경우 난감한 상황이 종종 발생한다.

 (1) FK를 맺을 때 다른 테이블에서 복합키 전부를 갖고 있거나, 중간 테이블을 하나 더 둬야 하는 상황이 발생한다.

 (2) 인덱스에 좋은 영향을 끼치지 못한다.

 (3) 유니크한 조건이 변경될 경우 PK 전체를 수정해야하는 일이 발생한다.

-> 주민등록번호, 복합키 등은 유니크 키로 별도로 추가하는 것을 추천한다.

 

(3) 롬복 라이브러리의 어노테이션

_5 @NoArgsConstructor : 기본 생성자 자동 추가, pubilc Posts(){}와 같은 효과

_6 @Getter : 클래스 내 모든 필드의 Getter 메소드를 자동생성

_7 @Builder : 해당 클래스의 빌더 패턴 클래스를 생성, 생성자 상단에 선언시 생성자에 포함된 필드만 빌더에 포함

 

 

서비스 초기 구축 단계에서는 테이블 설계(여기에선 Entity 설계)가 빈번하게 변경되는데, 이때 롬복의 어노테이션들은 코드 변경량을 최소화시켜주기 때문에 적극적으로 사용하도록 한다. 

 

그렇다면 Setter가 없는 상황에서 어떻게 값을 채워 DB에 삽입해야할까? 기본적인 구조는 생성자를 통해 최종값을 채운 후 DB에 삽입하는 것이며, 값 변경이 필요한 경우 해당 이벤트에 맞는 public 메소드를 호출하여 변경하는 것을 전제로 한다. 여기에서는 생성자 대신에 @Builder를 통해 제공되는 빌더 클래스를 사용한다. 생성자나 빌더나 생성 시점에 값을 채워주는 역할은 똑같다. 다만, 생성자의 경우 지금 채워야 할 필드가 무엇인지 명확히 지정할 수 없다.

 

예를 들어 다음과 같은 생성자가 있다면 개발자가 new Example(b,a)처럼 a와 b의 위치를 변경해도 코드를 실행하기 전까지는 문제를 찾을 수 없다.

public Example(String a, String b){
	this.a = a;
	this.b = b;
}

하지만 빌더를 사용하게 되면 다음과 같이 어느 필드에 어떤 값을 채워야 할지 명확하게 인지할 수 있다.

 

Example.builder()
	.a(a)
    .b(b)
    .build();


//다른 예시
public class CarImpl {

    private String id = "1";
    private String name = "carTest";

    Car car3 = Car.builder()
            .id(id)
            .name(name)
            .build();
}

앞으로 이렇게 빌더 패턴을 적극적으로 사용할 것이므로, 잘 익혀두는 것이 좋다. 


 

빌더 패턴

  • 복잡한 객체의 생성 과정 및 표현 방법을 분리해 동일한 생성 절차에서 서로 다른 표현 결과를 만들 수 있게 하는 패턴

빌더 패턴 장점

  • 어떤 필드에 어떤 값을 채워야 할지 명확히 지정할 수 있음
  • 필수 및 선택인자가 많아질수록 생성자 방식보다 가독성이 좋다
  • 자바빈 패턴(setter를 이용하는 방식)보다 안전함.
  • setter 생성을 방지하기 때문에 객체를 변경할 수 없음 (불변성 보장)

예시 1

import lombok.Builder;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class Car {

    private String id;
    private String name;

    @Builder    // 생성자를 만든 후 그 위에 @Builder 애노테이션 적용
    public Car(String id, String name) {
        this.id = id;
        this.name = name;
    }
}

이렇게 해서 간단하게 Lombok을 통해 @Builder 애노테이션을 생성자를 만든 후 적용 해준다.

public class CarImpl {

    private String id = "1";
    private String name = "carTest";

    Car car3 = Car.builder()
            .id(id)
            .name(name)
            .build();
}

그 후 이런식으로 생성자 파라미터 주입을 해준다.
이렇게 하면 각 인자에 대한 파라미터 주입이 명확해진다.

 

예시 2 : 

생성자 패턴 vs 빌더 패턴

1. 생성자 패턴 예시

public class Person {
	private String name;
	private int age;
	private String grade;
	
	public Person(String name, int age, String grade){
		this.name = name;
		this.age = age;
		this.grade = grade;
	}
	
	// Getter 
	public String getName() {
		return name;
	}
	public int getAge() {
		return age;
	}
	public String getGrade() {
		return grade;
	}	
}
String name = "jueun";
int age = 24;
String grade = "4";
Person p1 = new Person(name, age, grade);
// String type 변수가 위치가 바뀌어도 문제점 찾지 못함 
Person p2 = new Person(grade, age, name);

2. 빌더 패턴 예시

public class Person {
	
	// final 키워드를 사용해 생성자를 통한 입력 
	private final String name, grade;
	private final int age;
	
	// 클래스 내에 static 형태의 내부 클래스(inner class) 생성
	protected static class Builder{
		private String name;
		private String grade;
		private int age;
		
		//name 입력값 받음 : return type을 Builder로 지정 후 this return
		public Builder name(String value) {
			name = value;
			return this;
		}
		
		//grade 입력값 받음
		public Builder grade(String value) {
			grade = value;
			return this;
		}
		
		//age 입력값 받음 
		public Builder age(int value) {
			age = value;
			return this;
		}		
		
		// build() 메소드 실행하면 this가 return 되도록 지정  
		public Person build() {
			return new Person(this);
		}		
	}
	
	// 생성자를 private으로 함 
	// 외부에서 접근 x + Builder 클래스에서 사용 가능 
	private Person(Builder builder){
		name = builder.name;
		age = builder.age;
		grade = builder.grade;
	}
	
	// 빌더 소환 : 외부에서 Person.builder() 형태로 접근 가능하게 static method로 생성 
	public static Builder builder() {
		return new Builder();
	}
	
	//Geter 생략 
}
String name = "jueun";
int age = 24;
String grade = "4";
Person p1 = Person.builder()
		        .name(name)
		        .age(age)
		        .grade(grade)
		        .build();
}

3. 롬복으로 빌더 패턴 적용

package kr.pe.playdata.domain;

import lombok.Builder;
import lombok.Getter;

@Getter //Getter 생성 
public class LombokPerson {

	private  String name;
	private  String grade;
	private  int age;
	
	@Builder // 생성자 만든 후 위에 @Build 어노테이션 적용 
	public LombokPerson(String name, String grade, int age) {
		this.name = name;
		this.grade = grade;
		this.age = age;
	}
}