// JPA 사용 시
public interface QuestionRepository extends CrudRepository<Question, Long> {
}
실제로 DB에 접근하는 객체
→ Persistence Layer : DB에 data를 CRUD하는 계층
Service 와 DB 를 연결하는 고리의 역할
SQL 을 사용해서(개발자가 직접 코딩), DB에 접근한 후 적절한 CRUD API 를 제공
이런식으로 Controller 에서 Repository 를 호출하게 되면, 중복 코드가 발생하게 된다.
결론
Controller 에서 Repository 를 호출하지 말자.
온라인서점 사이트에서 책을 조회하고 구매한다고 가정해보자.
'온라인 서점'
--> 구현해야할 소프트웨어의 대상
--> 상품의 조회, 구매, 결제... 등의 기능을 제공
따라서, 온라인 서점은 소프트웨어로 해결하고자하는 문제 영역
= Domain(도메인)
도메인 모델
도메인 모델은 기본적으로 도메인 자체를 이해하기 휘한 개념모델이다.
그러므로 굳이 특정한 형태로 모델을 만들 필요는 없다.
즉,
도메인 모델(객체): 내가 개발하고자 하는 영역을 분석하고, 그 분석의 결과로 도출된 모델(객체)
이렇게 도출한 도메인 모델은 크게 Entity와 Value로 구분할 수 있다.
'Entity 클래스' or '가장 core한 클래스' 라고 부름
Entity는 식별자를 가진다.
Purchase는 주문id를 식별자로 가진다고 가정해보자.
Purchase에서 주문상태를 변경한다고해서 다른 주문이 되지는 않는다.
즉, 식별자 외 데이터(주문상태)가 변경된다고 해서
그 객체가 다른 객체가 되지않는다.
Entity 클래스는 실제 DB 테이블과 매핑되는 핵심 클래스
→ DB 테이블에 존재하는 컬럼들을 필드로 가지는 객체
(DB의 테이블과 1:1로 매핑되며, 테이블이 가지지 않는 컬럼을 필드로 가져서는 안 된다.)
Entity 는 DB 영속성(persistent)의 목적으로 사용되는 객체
→ 요청(Request)이나 응답(Response) 값을 전달하는 클래스로 사용해서는 X
서비스 클래스와 비즈니스 로직들이 Entity 클래스를 기준으로 동작하므로, Entity 클래스가 변경되면 여러 클래스에 영향을 줄 수 있다.
사용법 1
@Entity
@Data
@NoArgsConstructor
@Table(name = "legacy_user")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String email;
@Column(name="crtdat", nullable = false)
private LocalDateTime createdAt;
}
JPA 로 DB 를 사용하기 위해, 엔티티를 테이블로 지정해줄 때의 필수값
DB 의 테이블과 1:1로 매칭되는 객체 단위
→ Entity 객체의 인스턴스 하나가 테이블에서 하나의 레코드 값을 의미
기본 생성자는 필수
final 클래스, enum(enumerated type, 열거형), interface 등...에는 사용 X
저장할 필드라면 final 사용 X
주의!
이거 자체가 DB 테이블은 아니다.
자바의 객체일 뿐!
자동으로 기본키를 생성하는 방법에는 4가지가 있다.
→ 기본키를 자동으로 생성할 때에는 @Id와 @GenerratedValue 어노테이션이 함께 사용되어야 한다.
GenerationType.AUTO
GenerationType.IDENTITY
GenerationType.SEQUENCE
GenerationType.TABLE
name
index, uniqeConstraints 등... 그 외의 설정
JPA 로 테이블을 생성한다면, 적용이 되지만
기존 DB 테이블이 존재할 경우, indexes와 uniqueConstraints는 동작하지 X
즉,
기존 DB 테이블에서 해당 설정이 되어 있으면 작동하지만
없다면 전혀 적용되지 X
일반적으로 실제 운영에서는 해당 사항은 DB 설정으로만 남기고, 따로 @Table 에서는 설정하지 않는다.
생략 시, 매핑한 Entity 이름을 Table 이름으로 사용
객체 필드를 테이블 컬럼에 매핑하는데 사용
생략 가능: 이름을 지정 할 때 아니고는 보통 생략함
속성
name
nullable (DDL) --> 多 사용
null 값의 허용 여부 설정
false 설정 시, not null
@Column 사용 시, nullable = false 로 설정하는 것이 안전
Optional 과 기본적인 개념은 동일하다!
unique (DDL)
columnDefinition (DDL)
length (DDL)
percision, scale (DDL)
DB 테이블과 Spring에서 사용하는 칼럼명이 다를 경우, @Column 을 사용할 수 있다
name
내가 Spring에서 사용하고 싶은 칼럼명을 정의할 수 있다
nullable
default 가 nullable = true 이므로
값을 입력하고 싶다면 한다면, @Column(nullable = false)로 설정하여 null이 입력되면 오류가 나도록 설정할 수 있다.
nullable = false인 칼럼이 null로 DB에 들어간다면, 다음과 같이 not-null 칼럼이 null을 참조한다는 예외가 나옴
org.springframework.dao.DataIntegrityViolationException: not-null property references a null
or transient value : com.example.demo.domain.User.createdAt; nested exception is
org.hibernate.PropertyValueException: not-null property references a null or transient value :
com.example.demo.domain.User.createdAt
사용법 2 : Enum
public Enum Gender {
MALE, // 0
FEMALE // 1
}
// 순서 변경할 경우
public Enum Gender {
FEMALE, // 0
MALE // 1
}
// 중간에 데이터 추가할 경우
public Enum Gender {
FEMALE, // 0
MIX, // 1
MALE // 2
}
@Enumerated(EnumType.STRING)
를 사용해야 한다.사용법 3 : 생성/수정 시간
@EnableJpaAuditing // @EnableJpaAuditing을 통해, 해당 프로젝트가 Audit 기능을 사용을 설정
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
생성날짜, 수정날짜가 담긴 클래스 생성 & @CreatedDate와 @LastModifiedDate 설정
@Getter
@MappedSuperclass
@EntityListeners(value = AuditingEntityListener.class)
public abstract class BaseEntity {
@CreatedDate
private LocalDateTime createdAt;
@LastModifiedDate
private LocalDateTime updatedAt;
}
@Entity나 @MappedSuperclass가 설정된 클래스에 리스너를 추가해줌
Java Enum을 테이블에서 사용한다고 간주하기
속성
Entity
Table
데이터베이스나 SQL에 실제로 존재하며 물리적인 구조를 지님
물리 모델에서 사용
name 속성의 사용
@Entity(name = "...")
- 엔티티의 이름을 정한다
- @Table 없이 @Entity(name = "...")만 존재할 경우, 해당 @Entity의 name 속성에 의해 엔티티와 테이블의 이름이 모두 정해진다.@Table(name = "...")
- DB 에 생성될 테이블의 이름을 정한다
Entity
Value
계층(Layer) 간 데이터 교환을 하기 위해 사용하는 객체(Java Beans)
→ 계층 사이의 각각의 DTO 는 100% 무조건 분리돼있어야 한다.
직렬화에도 사용되는 객체(JSON serialization 등...)
로직(프로그램을 만들 때의 논리적인 흐름)을 가지지 않는 데이터 객체
getter/setter 메소드만 가진 클래스
(이 외의 비즈니스 로직은 포함 X)
DB에서 꺼낸 데이터를 저장하는 Entity를 가지고 만드는 일종의 Wrapper(데이터 타입 변환을 도움)
Entity를 Controller 같은 클라이언트단과 직접 마주하는 계층에 직접 전달하는 대신 DTO를 사용해 데이터를 교환
Request와 Response용 DTO는 View를 위한 클래스
Dto를 Entity 로 변환하여 DB 에 저장한다
→ toEntity() 메서드를 통해, DTO 에서 필요한 부분을 이용하여 Entity 로 만든다.
View Layer(객체를 표현하기 위한 계층, Controller Layer)와 DB Layer(저장하는 계층)의 역할을 분리하기 위해
Entity 객체의 변경을 피하기 위해
Entity 객체를 그대로 사용하면, 프로그래머의 의도와 다르게 데이터가 변질될 수 있다.
Entity 의 변경을 최소화하기 위해
View와 통신하는 DTO 클래스(예를 들어 ResponseDTO, RequestDTO)가 자주 변경된다.
(어떤 요청에서는 특정 값이 추가될 수도 있고 특정값이 없을 수도 있다.)
따라서, Entity 클래스와 분리하여 관리해야 한다.
도메인 모델링을 지키기 위해
Entity 클래스의 특정 컬럼들을 조합하여 특정 포맷을 출력하고 싶다 때
Entity 클래스에 표현을 위한 필드나 로직이 추가되면 객체 설계를 망가뜨릴 수 있으나,
DTO에 표현을 위한 로직을 추가해서 사용하면 Entity의 도메인 모델링을 지킬 수 있다.
private 필드에 접근하기 위해
public class MemberDto {
private int num;
private String name;
private String addr;
public void setNum(int num) { // num 필드에 대한 setter 메소드
this.num = num;
}
public void setName(String name) { // name 필드에 대한 setter 메소드
this.name = name;
}
public void setAddr(String addr) { // addr 필드에 대한 setter 메소드
this.addr = addr;
}
public int getNum() { // num 필드에 대한 getter 메소드
return num;
}
public String getName() { // name 필드에 대한 getter 메소드
return name;
}
public String getAddr() { // addr 필드에 대한 getter 메소드
return addr;
}
}
public class MainClass12 {
public static void main(String[] args) {
// MemberDto 객체 생성해서 참조값을 dto1 에 담기
MemberDto dto1 = new MemberDto();
// DTO를 활용하여, private 필드에서 setter 메소드로 num에 1 을 전달
dto1.setNum(1);
dto1.setName("김구라");
dto1.setAddr("노량진");
// 원래는 private 필드에 접근이 불가하나, dto1 에 저장된 값을 참조하여 getter 메소드로 활용
int num = dto1.getNum();
String name = dto1.getName();
String addr = dto1.getAddr();
}
}
// inner class
public class User{
class admin{
}
}
// inner static class
public class User{
static class admin{
}
}
inner class(내부 클래스)는 말 그대로 중첩 클래스 中 내부에 선언된 클래스
선언문
// inner class : 내부 클래스로 인스턴스 2개 생성
MyClass.InnerClass a = new MyClass().new InnerClass();
MyClass.InnerClass b = new MyClass().new InnerClass();
/* result: a != b (다른 참조) */
// inner static class : 내부 정적 클래스로 2개 생성
MyClass.InnerClass c = new MyClass.InnerStaticClass();
MyClass.InnerClass d = new MyClass.InnerStaticClass();
/* result: c != d (다른 참조) */
외부 참조
// inner class : 외부 클래스의 인스턴스를 만들고, 다시 InnerClass 생성 시 참조 가능
MyClass a = new MyClass();
MyClass.InnerClass b = a.new InnerClass(); // b 는 a 에 대한 숨은 외부 참조를 갖는다.
// inner static class : innerStaticClass 는 해당 X
MyClass.InnerStaticClass c = new MyClass.InnerStaticClass(); // c 는 해당 X
class MyClass {
void myMethod() {
...
}
// inner class 적용
class InnerClass{
void innerClassMethod() {
MyClass.this.myMethod(); // 내부 코드에서도 외부클래스의 속성 참조에서 차이를 보인다. 숨은 외부 참조가 있기 때문에 가능
}
}
// inner static class 적용
static class InnerStaticClass{
void innerClassMethod() {
MyClass.this.myMethod(); // 컴파일 에러
}
}
}
inner Class
1. 참조값을 담아야 하므로, 인스턴스 생성 시 시간적/공간적으로 성능 ↓
2. 외부 인스턴스에 대한 참조가 존재하므로, 가비지 컬렉션이 인스턴스 수거를 하지 못하여 메모리 누수가 생길 수 있다.
3. 바깥 인스턴스와 암묵적으로 연결이 되어 있으므로, 바깥 인스턴스 없이는 멤버 클래스를 생성할 수 없다. (클래스명.this)
inner static class
중첩 클래스의 내부 클래스가 바깥 인스턴스와 독립적으로 존재할 수 있다면,
정적 멤버 클래스로 만들어서 바깥 인스턴스로의 외부 참조를 가지지 않게 만들면 된다.
→ 외부 참조를 저장할 메모리가 사라지므로, 메모리 누수X 메모리 사용↓ 생성 시간↓
결론
innerClass 를 생성해야할 경우, 외부 클래스로 빼도록 하고
그게 아니면, 내부 클래스 생성 시 static으로 사용하는게 적합하다.
잘못된 사용
Member
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
public Member(final String name) {
this.name = name;
}
}
Dto
@Getter
@AllArgsConstructor
@NoArgsConstructor
public class MemberCreateRequest {
private String name;
}
@Getter
@AllArgsConstructor
public class MemberCreateResponse {
private Long id;
private String name;
}
이 경우에 Member를 create 하는 기능이 필요할 시,
MemberCreateRequest, MemberCreateResponse 벌써 두 개의 DTO 클래스가 필요해진다.
→ 여러 DTO 가 계속 필요해지는 상황이 온다면, MemberxxxRequest 이런 DTO 클래스가 점점 늘어나게 된다.
→ DTO 를 선별/구분하는데 인적 리소스가 소모되게 될 것이다.
올바른 사용
Member
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
public Member(final String name) {
this.name = name;
}
@Getter
@AllArgsConstructor
@NoArgsConstructor
public static class Request {
private String name;
}
@Getter
@AllArgsConstructor
public static class Response {
private Long id;
private String name;
}
}
inner class 를 이용해, Member 안에 Request와 Response DTO를 가지게 한다.
→ Member 클래스만 보고 관련된 DTO를 빠르게 찾을 수 있다.
→ 클래스 파일 개수↓ 인적 리소스↓ 개발의 편의성↑
Controller
@RestController
@RequestMapping("/api/member")
public class MemberController {
@GetMapping("/{id}")
public ResponseEntity<Member.Response> getMember(@PathVariable final Long id) {
return ResponseEntity.ok(new Member.Response(id, "unluckyjung"));
}
@PostMapping
public ResponseEntity<Member.Response> create(@RequestBody final Member.Request request) {
//...저장 로직
return ResponseEntity.created(URI.create(String.format("/api/member/%d", 1L)))
.body(new Member.Response(1L, request.getName()));
}
}
Controller 에서 inner class 를 요청/응답한다.
Entity 와 DTO 의 역할 차이 (DB Layer 와 View Layer 의 역할을 철저하게 분리)
Entity
DTO
정보가 불일치 할 경우
테이블에 mapping되는 정보와 실제 View에서 요청되는 정보가 다를 경우
테이블에 필요한 정보에 맞게 데이터를 변환하는 로직이 필요할 수 있는데, 이때 해당 로직이 Entity 에 들어가게 되면 지저분해진다.
정보 노출
DB 로부터 조회된 Entity 를 그대로 View로 넘길 경우
불필요한/노출되면 안 되는 정보까지 노출될 수 있고, 이를 막기 위한 로직을 따로 구현해야 한다.
@Getter
@Setter
@Alias("example")
class exampleVO {
private Long a;
private String b;
private String c;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Example example = (Example) o;
return Objects.equals(id, example.a);
}
@Override
public int hashCode() {
return Objects.hash(a);
}
}
값 자체를 표현하는 객체
객체들의 주소 값이 달라도, 데이터 값이 같으면 동일한 것으로 여긴다.
→ 내용물이 값 자체를 의미하므로, 'read only'의 특징을 가진다.
값 비교를 위해, equals()와 hashCode() 메서드를 오버라이딩
→ VO 내부에 선언된 속성(Field)의 모든 값들이 VO 객체마다 값이 같아야, 똑같은 객체라고 판별
DTO
인스턴스 개념
단지 데이터를 담는 그릇의 역할일 뿐 값은 그저 전달되어야 할 대상일 뿐
→ Layer 간의 통신을 위한 객체
→ 전달될 데이터를 보존해야함
VO
리터럴 값 개념
읽기만 가능한 read-only 속성을 가진 객체
→ 값들에 대해 Read-Only를 보장해줘야 존재의 신뢰성이 확보
특정 비즈니스 값을 담는 객체
→ 값 자체에 의미가 있음
참고: [Java] DTO(Data Transfer Object) or VO(Value Object) /getter(), setter()
참고: 역할 분리를 위한 Entity, DTO 개념과 차이점
참고: Entity, DTO, VO 차이
참고: [DAO] DAO, DTO, Entity Class의 차이
참고: [베네픽처 홈페이지] Controller, Repository 분리
참고: DTO 관리 - Inner class
참고: [Java] inner class 와 inner static class 차이
참고: DTO를 inner class로 관리하기
참고: Entity
참고: @Table과 @Entity 차이점
참고: @GeneratedValue 전략