03. RDBMS와 JPA, JPA Hibernate 프록시에 대해 알아보자
데이터베이스 제약 조건
제약조건 : 데이터의 무결성을 지키기 위해 입력받은 데이터에 대한 제한을 두는 것
사용자로부터 데이터가 입력된 경우, 제약조건이 설정되어 있다면 검사를 통해 조건을 만족했을 경우 DB에 데이터가 완전히 입력되도록 제약할 수 있다.
모든 제약 조건은 데이터 사전에 저장되며, 보통 테이블 생성 당시에 제약 조건을 명시하지만 생성 후에도 ALTER 명령어를 통해 수정/추가가 가능하다.
ALTER TABLE <테이블명> ADD CONSTRAINT <제약조건명>
ALTER TABLE <테이블명> DROP CONSTRAINT <제약조건명>
NOT NULL
해당 필드에 NULL 값을 저장할 수 없게 하는 제약조건
Create Table 테이블(
필드명 INT NOT NULL // 해당 필드에는 null 값이 저장되지 않음
);
UNIQUE
중복된 값을 저장할 수 없게 하는 제약조건
// 기본적인 Unique
Create Table 테이블(
필드명 INT UNIQUE
);
// 제약조건에 이름을 부여한 Unique
Create Table 테이블(
필드명 INT ,
...
CONSTRAINT 제약조건명 UNIQUE 필드명
);
PRIMARY KEY
기본키 지정 제약조건. PRIMARY KEY 제약조건을 가진 해당 필드는 NOT NULL과 UNIQUE 제약 조건의 특성을 모두 가짐
// 기본적인 Primary Key
Create Table 테이블(
필드명 INT PRIMARY KEY
);
// 제약조건에 이름을 부여한 Primary Key
Create Table 테이블(
필드명 INT ,
...
CONSTRAINT 제약조건 PRIMARY KEY 필드명
);
FOREIGN KEY
키본 키를 참조하는 컬럼을 지정하는 제약조건. 외래 키가 참조하는 기본 키의 컬럼과 데이터 형이 일치해야 함
Create Table 테이블(
필드명 INT ,
...
CONSTRAINT 제약조건 FOREIGN KEY (필드명)
REFERENCES 테이블명 (필드명)
);
외래키 제약조건을 가진 경우 해당 테이블의 삭제 및 수정을 진행할 때, 연관된 필드가 어떤 동작을 수행할지 지정해 줄 수 있음
왜래 키에 의해 참조되고 있는 기본 키는 삭제가 불가함
// 부모 데이터 수정/삭제 시 자식 데이터도 수정/삭제됨
ON UPDATE CASCADE
ON DELETE CASCADE
// 부모 데이터 삭제 및 수정 시 자식 테이블의 참조 컬럼을 null로 설정
ON DELETE SET NULL
ON UPDATE SET NULL
// 부모 데이터 삭제 및 수정 시 자식 테이블의 참조 컬럼을 default 값으로 설정
ON DELETE SET DEFAULT
ON UPDATE SET DEFAULT
// 자식 테이블이 참조하고 있다면 데이터 수정/삭제 불가
ON DELETE RESTRICT
ON UPDATE RESTRICT
DEFAULT
해당 필드의 디폴트 값을 설정해주는 제약조건
Create Table 테이블(
필드명 INT DEFAULT '기본값'
);
관계형 데이터베이스의 정규화
정규화 : 관계형 데이터베이스의 설계에서 중복을 최소화하도록 데이터를 구조화하는 프로세스. 이상 현상의 발생 가능성을 감소시키지만 연산 시간이 증가한다는 단점이 있음
- 기본 정규형 : 제1정규형, 제2정규형, 제3정규형, BCNF(보이스/코드 정규형)
- 고급 정규형 : 제4정규형, 제5정규형
제1정규형
릴레이션에 속한 모든 속성의 도메인이 더이상 분해되지 않는 원자값으로만 구성된 정규형
제2정규형
기본키가 아닌 모든 속성이 기본키에 완전 함수 종속되는 정규형
부분 함수 종속을 제거하는 정규화 과정을 거쳐 만들어짐
- 완전 함수 종속 : 어떤 속성이 기본키에 대해 완전히 종속일 때
- 부분 함수 종속 : 어떤 속성이 기본키가 아닌 다른 속성에 종속되거나, 기본키가 여러 속성으로 구성되어 있을 경우 기본키를 구성하는 속성 중 일부만 종속될 때
제3정규형
기본키가 아닌 모든 속성이 기본키에 이행적 함수 종속이 되지 않는 정규형
- 이행적 함수 종속
- A → B , B → C 인 경우 A → C 가 성립될 때
- 즉, A를 알면 B를 알고 그를 통해 C를 알 수 있는 경우를 의미
BCNF
릴레이션의 함수 종속 관게에서 모든 결정자가 후보키이면 BCNF에 속한다.
하나의 릴레이션에 여러개의 후보키가 존재할 수도 있는데, 이런 경우는 제3정규형까지 모두 만족하더라도 이상 현상이 발생할 수 있다. 이러한 이상현상을 해결하기 위해 제3정규형보다 좀 더 엄격한 제약조건을 제시한 것이 BCNF이다.
ORM의 장단점
ORM : Object Relational Mapping의 약자로, 객체-관계 매핑을 의미
객체-관계 매핑을 좀 더 설명하자면, 객체 지향 프로그래밍에서 쓰이는 객체라는 개념을 구현한 클래스와 관계형 데이터베이스에서 쓰이는 데이터인 테이블을 자동으로 매핑하는 것을 의미한다. 그러나 클래스와 테이블은 서로가 기존부터 호환가능성을 두고 만들어진 것이 아니기 때문에 불일치가 발생하는데, 이를 ORM을 통해 객체 간의 관계를 바탕으로 SQL 문을 자동으로 생성하여 불일치를 해결한다. 따라서 ORM을 이용하면 따로 SQL 문을 작성할 필요 없이 객체를 통해 간접적으로 데이터베이스를 조작할 수 있게 되는 것이다.
장점
- 완벽한 객체지향적 코드
- 개발자가 DB보다 객체에 집중해서 프로그래밍 할 수 있으며, 코드의 가독성을 높일 수 있음
- 재사용, 유지보수, 리팩토링 용이성
- ORM은 기존 객체와 독립적으로 작성되어있고, 객체로 작성되었기 때문에 재활용할 수 있음
- 매핑하는 정보가 명확하기 때문에 ERD를 보는 의존도를 낮출 수 있다.
- DBMS 종속성 하락
- 객체에만 집중할 수 있기 때문에 DBMS를 교체하는 큰 작업에도 리스크가 적고 드는 시간도 줄어든다.
단점
- ORM으로 완벽한 서비스를 구현하기 어려움
- 개발자가 DB보다 객체에 집중해서 프로그래밍 할 수 있으며, 코드의 가독성을 높일 수 있음
- 프로시저가 많은 시스템에서 ORM의 객체 지향적인 장점을 활용하기 어려움
- 프로시저는 데이터베이스에 대한 일련의 작업을 정리한 절차를 RDBMS에 저장한 것으로, 다시 객체로 바꾸는 과정에서 생산성 저하나 리스크가 많이 발생할 수 있음
- 프로시저는 데이터베이스에 대한 일련의 작업을 정리한 절차를 RDBMS에 저장한 것으로, 다시 객체로 바꾸는 과정에서 생산성 저하나 리스크가 많이 발생할 수 있음
@DynamicUpdate
@DynamicUpdate : JPA Entity에 사용하는 어노테이션으로, 실제 값이 변경된 컬럼으로만 update 쿼리를 만드는 기능
JPA의 기본 동작은 변경되지 않은 컬럼도 update 쿼리에 포함하기 때문에 이 어노테이션이 필요한 경우가 있다.
아래 예제를 통해 확인해보자.
Comment
@Entity
class Comment(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
var id: Long = 0,
var body: String,
var likeCount: Int,
var createdAt: LocalDateTime
)
CommentRepository
interface CommentRepository : JpaRepository<Comment, Long> { }
Test
@Test
@Transactional
@Rollback(false)
fun `dynamic update 결과 로그`() {
val comment = commentRepository.findByIdOrNull(1)
?: throw EntityNotFoundException()
comment.likeCount = comment.likeCount + 1
}
실행결과
Hibernate: select comment0_.id as id1_1_0_, comment0_.article_id as article_5_1_0_, comment0_.body as body2_1_0_, comment0_.created_at as created_3_1_0_, comment0_.like_count as like_cou4_1_0_, article1_.id as id1_0_1_, article1_.body as body2_0_1_ from comment comment0_ left outer join article article1_ on comment0_.article_id=article1_.id where comment0_.id=?
Hibernate: update comment set article_id=?, body=?, created_at=?, like_count=? where id=?
실행 결과를 보면 likeCount의 값만 변경을 했음에도 불구하고, set 안의 body와 createdAt도 새로운 변경값을 넣어주고 있는 것을 알 수 있다. 이제 Comment에 @DynamicUpdate를 추가해서 다시 실행해보자.
실행결과
Hibernate: select comment0_.id as id1_1_0_, comment0_.article_id as article_5_1_0_, comment0_.body as body2_1_0_, comment0_.created_at as created_3_1_0_, comment0_.like_count as like_cou4_1_0_, article1_.id as id1_0_1_, article1_.body as body2_0_1_ from comment comment0_ left outer join article article1_ on comment0_.article_id=article1_.id where comment0_.id=?
// update 쿼리의 set부분에 실제로 변경한 like_count만 설정이 추가
Hibernate: update comment set like_count=? where id=?
정규표현식
정규표현식 : 특정한 규칙을 가진 문자열의 집합을 표현하는 데 사용하는 형식 언어로, regexp 또는 regex라고 부름
정규표현식은 주로 특정 목적을 위해 필요한 문자열 집합을 지정하기 위해 쓰이는 식이다. 보통 회원가입 시 아이디 혹은 비밀번호의 조건을 걸어주고 싶을 때 이를 쉽게 해주는 용도로 사용한다.
정규표현식 연습 사이트
RegExr: Learn, Build, & Test RegEx
RegExr is an online tool to learn, build, & test Regular Expressions (RegEx / RegExp).
regexr.com
Groups and Ranges
Quantifiers
Boundary-type
Character classes
정규표현식 강의 추천!
https://www.youtube.com/watch?v=t3M6toIflyQ
GO SOPT 3차 세미나에서 사용한 정규표현식 해석
regexp="(?=.*[0-9])(?=.*[a-zA-Z])(?=.*\\W)(?=\\S+$).{8,20}",
message = "비밀번호는 영문 대,소문자와 숫자, 특수기호가
적어도 1개 이상씩 포함된 8자 ~ 20자의 비밀번호여야 합니다."
(?=.*[0-9]) : 숫자가 1개 이상 들어가야 함
(?=.*[a-zA-Z]) : 알파벳(대소문자)이 1개 이상 들어가야 함
(?=.*\W) : 특수문자가 1개 이상 들어가야 함
(?=\S+$) : 공백 없이 문자열 전체가 일치해야 함
.{8,20} : 최소 8자에서 최대 20자까지의 문자열이어야 함
- ?=
- 전방 탐색(Lookahead)을 의미하며, 작성한 패턴에 일치하는 영역이 존재해도 그 값을 반환하지 않음
- .*
- 1개 이상을 의미
- \W
- 특수문자를 의미
- \S
- 공백문자가 아닌 문자를 의미
- .{8,20}의 .
- 앞의 전방탐색 구문에서 조건을 만족하는 문자열 중에서 라는 의미
프록시
프록시 : '대신하다'라는 의미를 가진 단어로, JPA 하이버네이트에서는 동작을 대신해주는 가짜 객체의 개념으로 쓰임
하이버네이트는 지연 로딩을 구현하기 위해 프록시를 사용한다. 지연 로딩을 하려면 연관된 엔티티의 실제 데이터가 필요할 때까지 조회를 미뤄야 하는데, 그렇다고 해당 엔티티를 연관관계로 가지고 있는 엔티티의 필드에 null 값을 넣어 둘 수는 없다. 이때 하이버네이트는 지연 로딩을 사용하는 연관관계 자리에 프록시 객체를 주입하여 실제 객체가 들어있는 것처럼 동작하도록 한다.
프록시는 어떻게 실제 객체처럼 동작할 수 있을까?
이는 프록시가 실제 객체를 상속한 타입을 가지고 있기 때문이다. 프록시 객체는 실제 객체에 대한 참조를 보관하여, 프록시 객체의 메서드를 호출했을 때 실제 객체의 메서드를 호출한다. 이를 통해 JPA 엔티티 생성의 중요 규칙이 만들어지기도 했는데, '기본 생성자는 최소 protected 접근 제한자를 가져야 한다.'는 규칙과 '엔티티 클래스는 final로 정의할 수 없다.'가 그 규칙이다. 만약 기본 생성자가 private이면 프록시 생성 시 super를 호출할 수 없을 것이고, 엔티티를 final로 선언한다면 상속이 불가능하게 되기 때문이다.
프록시의 초기화는 어떻게 이루어질까?
최초 지연 로딩의 시점에는 당연히 참조 값이 존재하지 않는다. 때문에 실제 객체의 메서드를 호출할 필요가 있을 때 DB를 조회해서 참조 값을 채우게 되는데, 이를 프록시 객체를 초기화한다고 한다. 실제 객체의 메서드를 호출할 필요가 있을 때 select 쿼리를 실행하여 실제 객체를 데이터베이스에서 조회해오고, 참조 값을 저장하게 되는 것이다.
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column
private String name;
@JoinColumn(name = "team_id")
@ManyToOne(fetch = FetchType.LAZY) // 지연 로딩
private Team team;
public Member(String name, Team team) {
this.name = name;
this.team = team;
}
...
}
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class Team {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column
private String name;
public Team(String name) {
this.name = name;
}
...
}
Member의 연관관계 Team은 지연 로딩으로 설정되어 있기 때문에, Member를 조회해오게 되면 Team 필드 자리에는 프록시가 들어있다. 이 때 프록시 Team의 getName 메서드를 호출하게 되면 select 쿼리가 실행되고 프록시가 초기화된다.
System.out.println("========");
Team team = member.getTeam();
System.out.println(team.getName()); // 이 시점에 프록시 초기화!
System.out.println("========");
Team team = member.getTeam();
System.out.println(team.getId()); // 예외 : 식별자를 조회할 때는 프록시가 초기화되지 않음
하지만, 이 때 프록시가 실제 객체를 참조하게 되는 것이지 프록시가 실제 객체로 바뀌는 것이 아니라는 점을 주의해야 한다. 참고로 프록시의 초기화는 영속성 컨텍스트의 도움을 받기 때문에 준영속 상태의 프록시를 초기화 한다거나, OSIV 옵션을 끈 경우 트랜잭션 바깥에서 프록시를 초기화 하려고 한다면 LazyInitializationException을 만나게 될 수 있다. 때문에 프록시를 초기화 할 때에는 반드시 프록시가 영속 상태인지 확인하자!