import java.util.*;
class Solution {
static boolean v[][]; // 체크 배열
public int solution(int m, int n, String[] board) {
int answer = 0;
char copy [][] = new char[m][n];
for(int i = 0; i < m; i++) {
copy[i] = board[i].toCharArray();
}
boolean flag = true;
while(flag) {
v = new boolean[m][n];
flag = false;
for(int i = 0; i < m-1; i++) {
for(int j = 0; j < n-1; j++) {
if(copy[i][j] == '#') continue; // #은 빈칸을 의미
if(check(i, j, copy)) {
v[i][j] = true;
v[i][j+1] = true;
v[i+1][j] = true;
v[i+1][j+1] = true;
flag = true;
}
}
}
answer += erase(m,n,copy);
v = new boolean[m][n];
}
return answer;
}
/* 2*2가 같은지 체크 */
public static boolean check(int x, int y, char[][] board) {
char ch = board[x][y];
if(ch == board[x][y+1] && ch ==board[x+1][y] && ch == board[x+1][y+1]) {
return true;
}
return false;
}
public static int erase(int m, int n, char [][] board) {
int cnt = 0;
for(int i = 0; i < m; i++) {
for(int j = 0; j < n; j++) {
if(v[i][j]) {
board[i][j] = '.';
}
}
}
/* 큐를 이용해 세로로 제거 작업 진행 */
for(int i = 0; i < n; i++) {
Queue<Character> q = new LinkedList<>();
for(int j = m-1; j >=0; j--) {
if(board[j][i] == '.') {
cnt++; // 지우는 블록 카운트
} else {
q.add(board[j][i]);
}
}
int idx = m-1;
// 삭제한 블록 위의 블록들 내리기
while(!q.isEmpty()) {
board[idx--][i] = q.poll();
}
// 빈칸 채우기
for(int j = idx; j >=0; j--) {
board[j][i] = '#';
}
}
return cnt;
}
}
시뮬레이션 유형
으로 특정 규칙에 따라 블록이 지워지고 이동하는 과정을 반복하며 최종적으로 몇 개의 블록이 지워지는지를 구하는 것이 목표다.
문제에서는 다음과 같은 매개변수가 주어지고, 2가지를 기준으로 코드를 작성하면 된다.
판의 높이 = m
폭 = n
판의 배치 정보 = board
2*2블록이 같은지 체크하는 함수
현재 블록을 기준으로 오른쪽, 아래, 오른쪽 아래가 같은지 체크하면 된다.
이때 조심할 것은 배열의 범위를 벗어나지 않도록 해야 한다.
만약 2*2 블록이 같다면 이를 기록할 boolean[][] 배열에 따로 표시를 한다.
직접적으로 board 배열을 바꾸지 않는 이유는 모든 블록을 조사한 후에 일괄적으로 처리해야 하기 때문이다.
블록을 제거하는 함수
큐
를 이용해 세로로 블록을 제거하는 방법을 사용했다.
맨 아래에서부터 큐에 집어넣고, 제거해야 하는 블록이라면 개수만 세고 큐에 넣지 않는다.
이후에 큐에 있는 원소를들 빼내서 아래부터 차곡차곡 블록을 쌓아준다.
빈 블록은 ‘#’으로 표시해준다.
이 글은 내 코드가 그렇게 이상한가요? 책을 읽고 정리한 내용을 바탕으로 작성하였습니다.
시스템
사전 정의: 수많은 구성 요소로 이루어진 집합체로, 각각의 부분이 유기적으로 연결되어, 전체적으로 하나의 목적을 갖고 움직이는 것
시스템은 목적 달성을 위한 수단
모델: 특정 목적 달성을 위해서, 최소한으로 필요한 요소를 갖춘 것
모델링: 모델의 의도를 정의하고, 구조를 설계하는 것
모델은 대상이 아니라 목적 달성의 수단이다.
목적
중심으로 이름을 잘 설계하면, 목적을 달성하기에 적절한 모델을 설계할 수 있다.단일 책임 원칙 == 단일 목적 원칙
모델을 검토할 때 목적 이외에 요소가 들어가 있다면 다시 수정한다.
모델 != 클래스 => 모델 하나는 여러 개의 클래스로 구성된다. (1:N)
예를 들어, 모델 - 상품이면, 클래스 - 상품 ID, 상품명, 판매 가격, 재고 수량
클래스 설계와 구현에서 무언가를 깨닫는다면, 이를 모델에 피드백해야 한다.
피드백 사이클을 계속 돌리는 것이 설계 품질을 높이는 비결이다.
기능성
은 소프트웨어의 품질 특성 중 하나로, 고객의 니즈를 만족하는 정도를 말한다.
숨어있는 목적 파악하기
상품 구매를 하면, ‘상품 구입’과 ‘구매 품목’에 대해서만 생각할 수 있지만, 법적인 요소도 고려해야 한다.
위처럼 기능을 제대로 발휘하려면, ‘개념의 정체’와 ‘뒤에 숨어 있는 중요한 목적’을 잘 파악해야 한다.
목적 달성
수단으로 해석하면, 추상화 했을 때 모델의 확장성이 커진다.
예를 들어, 이동 수단
이라는 목적을 추상화 했다면, 구체화로는 이족 보행, 마차, 전차, 전기 자동차, 비행기가 있다.
리팩터링
이란 실질적인 동작은 유지하면서, 구조만 정리하는 작업이다.중첩을 제거하여 보기 좋게 만든다.
의미 단위로 로직을 정리한다. -> 조건 확인과 값 대입 로직을 각각 분리해서 정리한다.
조건을 읽기 쉽게 한다. -> 논리 부정 연산자 !
를 사용하는 것처럼 한번 더 생각하게 되는 요소가 있으면 메서드로 추출해서 읽기 쉽게 한다.
목적을 나타내는 메서드로 만들어서 사용한다. -> 보유 포인트가 부족한지 리턴하는 메서드 = isShotOfPoint
단위 테스트
는 작은 기능 단위로 동작을 검증하는 테스트를 의미하며, 일반적으로 ‘테스트 프레임워크와 테스트 코드를 활용해서 메서드 단위로 동작을 검증하는 방법’이라 한다.
JUnit
을 사용한다)‘리팩터링을 할 때 단위 테스트는 필수다!’라는 말이 있을 정도로 리팩터링
과 단위 테스트
는 항상 세트이다.
테스트 코드를 사용한 리팩터링 흐름
이상적인 구조의 클래스 기본 형태를 어느 정도 잡는다.
이 기본 형태를 기반으로 테스트 코드를 작성한다.
테스트를 실패시킨다.
테스트를 성공시키기 위한 최소한의 코드를 작성한다.
테스트가 성공할 수 있도록, 조금씩 로직을 이상적인 구조로 리팩터링한다.
기능 추가와 리팩터링을 동시에 하지 않는다.
작업을 할 때는 ‘기능 추가(adding function)’와 ‘리팩터링(refactoring)’ 중에서 하나만 쓰고 있어야 한다. - 리팩터링 2판, 17.1.3절 -
리포지토리에 커밋할 때 기능 추가와 리팩터링을 따로 구분해야 한다.
작은 단계(small step)로 실시하는 것이 좋다.
리팩터링으로 메서드 이름 변경과 로직 이동을 했다면, 커밋을 따로따로 구분하는 것이 좋다. -> 커밋1: 메서드 이름 변경, 커밋2: 로직 이동
여러 번 커밋했다면, 풀 리퀘스트(Pull Request)
를 작성하는 것이 좋다.
리팩터링 시 언어와 프레임워크의 특성을 고려한 설계와 적용 방법을 생각해 보는 것이 중요하다.
소프트웨어의 제품과 관련된 품질 특성은 다음과 같다.
설계
는 ‘어떠한 문제를 효율적으로 해결하는 구조를 만드는 것’을 의미한다.
그렇다면 소프트웨어에서 설계
란, ‘어떤 소프트웨어의 품질 특성을 향상시키기 위한 구조를 만드는 것’이라고 말할 수 있다.
이 책은 소프트웨어 개발에서 나타날 수 있는 악마를 퇴치하는 설계 방법을 설명했다.
이러한 악마의 설징과 가장 관련 있는 품질 특성은 유지 보수성
이다.
유지 보수성
은 시스템이 정상 운용되도록 유지 보수하기가 얼마나 쉬운가를 나타내는 정도를 말한다.
유지 보수성 중에서도 특히 변경 용이성을 목적으로 하는 설계 방법을 다루어 온 것이다.
변경 용이성 설계를 하지 않으면, 개발 생산성이 저하되는데, 저하 요인으로는 크게 2가지가 있다.
요인 1: 버그가 발생하기 쉬운 구조
요인 2: 가독성이 낮은 구조
제대로 설계하지 않으면, 로직 변경과 디버그에 많은 시간을 소비하게 된다.
소프트웨어의 성장 가능성을 높이는 것이 바로 이 책의 핵심 주제이자 의의.
‘엔지니어’에게 ‘자산’이란 기술력이라고 생각함.
레커시 코드
는 프로젝트 발전과, 고품질 설계 경험을 막는다. 또한 시간을 낭비하게 만든다.
이러한 레거시 코드
는 기술 향상을 막고, 엔지니어에게 정말 중요한 자산이라고 할 수 있는 기술력의 축적을 막는다.
문제는 항상 이상과 현실의 차이 때문에 발생한다.
따라서 이상이 무엇인지 알고 있다면, 현실과 비교하며 차근차근 문제를 해결할 수 있다.
이와 같이 설계에서도 이상적인 설계와 현재 설계를 비교하면, 기술 부채를 인식할 수 있다.
루비의 코드 분석 라이브러리 RuboCop
에 따르면, 줄 수 상한이 메서드
를 기준으로 10줄 이내이고, 클래스
를 기준으로는 100줄 이내로 판단한다.
줄 수가 너무 많으면, ‘메서드와 클래스 분할’을 검토하자.
분할했으면, 분할한 클래스 하나하나가 일관되고 정상적인 동작을 하는 구조를 가져야 한다.
“클래스를 분할하면 읽기 어려워질까?” -> 클래스를 분할한 이후에, 클래스 하나하나가 정상적으로 동작하도록 설계하게 되면, 신뢰성이 높아져서 내부 로직에 대해 신경이 줄어들게 된다. -> 읽기가 더 쉬워지게 된다.
순환 복잡도
는 코드의 구조적인 복잡함을 나타내는 지표이다.
조건 분기, 반복 처리, 중첩이 많아지면 복잡도가 커진다.
순환 복잡도가 10이하 -> 굉장히 좋은 구조이고 버그가 발생할 확률은 25%이하이다.
응집도
는 모듈 내부에서 데이터와 로직이 관련되어 있는 정도를 나타내는 지표이다.
클래스로 해석하면, 클래스 내부에서 데이터와 로직의 관계가 얼마나 강한지 나타내는 지표이다.
응집도가 높을수록 변경 용이성이 높고 좋은 구조이다.
결합도
는 모듈 간의 의존도를 나타내는 지표이다.
클래스로 해석하면, 결합도는 ‘어떤 클래스가 호출하는 다른 클래스의 수’라고 볼 수 있다. (A라는 클래스가 B, C, D를 호출하고 있다라고 하면, 3개를 의존하게 된다)
의존하고 있는 클래스가 많으면 많을수록, 즉 결합도가 높을수록 더 넓은 범위를 고려해야 하므로, 유지 보수와 사양 변경이 어렵다.
결합도가 너무 높으면 단일 책임 원칙
을 위배하므로, 의존을 더 줄일 수 없는지, 클래스를 더 적게 분할할 수 없는지 검토해봐야 한다.
커뮤니케이션이 부족하면 버그가 많아지는 경향이 있다.
콘웨이 법칙
이란 ‘시스템의 구조는 그것을 설계하는 조직의 구조를 닮아 간다’는 접근 방법을 말한다.
예를 들어 개발 부문이 3개의 팀으로 구분되어 있다면 모듈이 수도 팀의 수와 동일하게 3개로 구성되는 시스템이 만들어진다는 것이다.
즉, 시스템의 구조가 릴리스 단위, 즉 팀 단위의 구조처럼 구성된다.
반대로, 역콘웨이 법칙
은 ‘소프트웨어의 구조를 먼저 설계하고, 이후 소프트웨어의 구조에 맞게 조직을 편성한다’는 접근 방법을 말한다.
팀원 간 관계 개선에는 심리적 안정성
이 중요하다.
심리적 안정성
이란 어떤 발언을 했을 때, ‘부끄럽거나 거절당하지 않을 것이라는 심리 상태’, ‘안심하고 자유롭게 발언 또는 행동할 수 있는 상태’ 등으로 정의한다.
성공적인 팀을 구축할 때 매우 중요한 개념이라고 알려져 있다.
‘빨리 끝내고 싶다’는 심리로 인해 설계 품질을 저하되게 만드는데, 이는 바람직하지 않다.
품질에 신경쓰지 않으면, 시간이 지날수록 구현 속도가 느려지게 된다. 나쁜 코드로 인한 구현 때문에 코드 수정이 다른 곳에 영향을 미치기 때문이다.
TDD
를 기반으로 클래스 설계와 구현 피드백 사이클을 돌리면서 설계 품질을 향상 시킨다.
한 번에 완벽하게 설계하지 않고 사이클을 돌려가며 완성한다. (한 번에 완벽하게 설계하려는 욕심 버리기!)
피드백 사이클을 돌며 개발 할 때는 개발 방식에 대해 처음부터 확실하게 합의하고 시작하는 것이 좋다.
너무 빠른 최적화(premature optimization)는 지양하자. - 병목이 어디인지 모른 채 성능이 빠른 코드를 작성하려고 하는 것
대부분의 경우에서 클래스 쪼개기가 성능에 미치는 영향이 전혀 없거나 거의 없다.
다수결로 설계 규칙을 만들지 말자.
설계 규칙
을 정할 때 시니어 엔지니어처럼 설계 역량이 뛰어난 팀원
이 중심이 되어 규칙을 만드는 것이 좋다.
다수결로 코드와 설계를 결정하려고 하면, 아무래도 수준이 낮은 쪽에 맞춰서 하향평준화되기 쉽기 때문이다.
각각의 설계 규칙에는 이유와 의도를 함께 적는 것이 좋다. -> 아무런 의미를 갖지 않는 상황을 막기 때문
팀의 설계 역량이 성숙하지 않으면, 개인에게 맡기지 말고, 설계를 어느 정도 아는 팀원
이 설계 리뷰와 코드 리뷰를 하도록 해서, 설계 품질을 관리할 수 있게 한다.
팀 구성원의 설계 역량이 어느 정도 성숙해지면, 다시 한번 설계 규칙에 대해 논의해보는 것도 좋다.
이 글은 [자바 ORM 표준 JPA 프로그래밍 - 기본편] 강의를 듣고 정리한 내용입니다.
관계형 데이터베이스에는 상속 관계가 없다.
대신에 관계형 데이터베이스는 슈퍼타입 서브타입 관계라는 모델링 기법이 객체 상속과 유사하다.
왼쪽이 슈퍼타입 서브타입 논리 모델
이고, 오른쪽이 객체 상속 모델
이다.
상속관계 매핑
이란 관계형 데이터베이스의 슈퍼타입 서브타입 논리 모델을 실제 물리 모델로 구현하는 방법을 말한다.
상속관계 매핑하는 방법에는 3가지 방법이 있다.
조인 전략
: 각각 테이블로 변환
단일 테이블 전략
: 통합 테이블로 변환
구현 클래스마다 테이블 전략
: 서브타입 테이블로 변환
@Inheritance(strategy=InheritanceType.XXX)
JOINED
: 조인 전략 (InheritanceType.JOINED)
SINGLE_TABLE
: 단일 테이블 전략 (InheritanceType.JOINED)
TABLE_PER_CLASS
: 구현 클래스마다 테이블 전략 (InheritanceType.TABLE_PER_CLASS)
@DiscriminatorColumn(name=“DTYPE”)
: 부모에서 구분 컬럼을 지정할 때 사용한다. 기본값이 DTYPE
이므로 name 속성은 생략 가능하나, 작성하는 것이 협업하기 있어서 좋다.
@DiscriminatorValue(“XXX”)
: 자식에서 구분 컬럼에 입력할 값을 지정할 때 사용한다. 기본값은 엔티티 이름
이다.
@PrimaryKeyJoinColumn(name = "Book_ID")
: 기본 값으로 자식 테이블은 부모 테이블 id 컬럼명을 그대로 사용하나, 변경 시 해당 설정값을 추가해줘야 한다.
조인 전략
은 엔티티마다 모두 테이블로 만들어주고 부모의 기본키를 기본키 + 외래키
로 사용한다.
그래서 조회할 때 조인을 사용한다.
다만 객체는 타입이 있는데 테이블은 타입에 개념이 없어서 따로 컬럼을 추가해줘야 한다.
@DiscriminatorColumn(name = "DTYPE")
// Item 엔티티
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn(name = "DTYPE")
public abstract class Item{
@Id @GeneratedValue
@Column(name = "ITEM_ID")
private Long id;
private String name;
private int price;
...
}
// Album 엔티티
@Entity
@DiscriminatorValue("A")
public class Album extends Item {
private String artist;
...
}
// Movie 엔티티
@Entity
@DiscriminatorValue("M")
public class Movie extends Item {
private String director;
private String actor;
...
}
// Book 엔티티
@Entity
@DiscriminatorValue("B")
public class Book extends Item {
private String author;
private String isbn;
}
...
Item 엔티티에서 상속 매핑을 하기 위해서 @Inheritance
추가하고, 조인 전략을 사용하기 위해 InheritanceType.JOINED
을 추가한다.
그리고 부모에서 구분 컬럼을 지정한다. -> @DiscriminatorColumn(name = "DTYPE")
자식들은 구분 컬럼을 어떻게 저장할지 작성한다.
Album 엔티티
는 부모로부터 DTYPE에 A
라는 값으로 저장된다. -> @DiscriminatorValue("A")
Movie 엔티티
는 부모로부터 DTYPE에 M
라는 값으로 저장된다.
Book 엔티티
는 부모로부터 DTYPE에 B
라는 값으로 저장된다.
자식 엔티티에는 Id가 없는데 부모 테이블에서 Id를 자식 테이블에서 그대로 사용한다.
@PrimaryKeyJoinColumn
을 사용한다.// Book 엔티티
@Entity
@DiscriminatorValue("B")
@PrimaryKeyJoinColumn(name = "BOOK_ID")
BOOK_ID
(이전: ITEM_ID)로 된다.장점
테이블이 정규화가 된다.
외래 키 참조 무결성 제약조건을 활용할 수 있다. -> 외래 키 참조로 ITEM_ID
로 ITEM 테이블만 확인해도 된다.
참조 무결성 제약조건
이란 자식 릴레이션의 외래 키의 값은 참조된 부모 릴레이션의 기본 키 값과 같아야 하며, 자식 릴레이션의 값이 변경될 때 부모 릴레이션의 제약을 받는다는 조건을 말한다.
외래 키
는 NULL 값이거나 또는 부모 테이블의 기본 키 or 고유 키 값과 동일해야 한다.
저장공간을 효율적으로 사용할 수 있다.
단점
조회시 조인을 많이 사용해서 성능이 저하된다.
조회 쿼리가 복잡하다.
데이터 저장시 INSERT SQL을 2번 호출한다.
단일 테이블 전략
은 말 그대로 테이블을 하나로 합치는 전략이다.
구분 컬럼(DTYPE)을 통해 어떤 자식 데이터가 저장되었는지 알 수 있다.
조인 전략과 형식은 비슷한데, 부모 테이블에 @Inheritance
에 (strategy = InheritanceType.SINGLE_TABLE)
을 지정한다.
또한 모든 테이블이 하나로 통합되어 있기 때문에 구분 컬럼
은 필수다.
// Item 엔티티
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DisciminatorColumn(name = "DTYPE")
...
// 자식 엔티티
@Entity
@DiscriminatorValue("A")
...
@Entity
@DiscriminatorValue("M")
@Entity
@DiscriminatorValue("B")
...
장점
테이블이 하나이므로 조인이 필요없기 때문에 일반적으로 조회 성능이 빠르다.
조회 쿼리가 단순하다.
단점
자식들을 모두 하나의 테이블로 만들었기 때문에, 자식들마다 컬럼이 달라서 null 값이 많을 수 있다. -> null 값 허용 예) Album 엔티티에는 ACTOR 컬럼을 쓰지 않아 null 이다.
단일 테이블에 모든 것이 저장되므로 테이블이 커질 수록 조회 성능이 오히려 느릴 수 있다.
구현 클래스마다 테이블 전략
은 자식 테이블이 부모 테이블의 모든 것을 다 가지고 있는 형태이다. 그래서 자식 엔티티마다 테이블을 다 만들어준다.// Item 엔티티
@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
...
// 자식 엔티티
@Entity
@DiscriminatorValue("A")
...
@Entity
@DiscriminatorValue("M")
@Entity
@DiscriminatorValue("B")
...
@Inheritance
에 (strategy = InheritanceType.TABLE_PER_CLASS)
을 지정한다.장점
서브 타입을 구분해서 처리할 때 효과적이다.
not null 제약조건을 사용할 수 있다. (각각의 테이블을 가지고 있기 때문)
단점
여러 자식 테이블을 함께 조회할 때 성능이 느리다. (SQL에 UNION
을 사용한다)
자식 테이블을 통합해서 쿼리하기가 어렵다.
위에서는 부모-자식 클래스 모두 데이터베이스 테이블과 상속 관계로 매핑했는데, 여기선 다르다.
객체 입장에서 부모 클래스를 자식 클래스에게 매핑 정보만 제공하고 싶을때, @MappedSuperclass
를 사용한다.
@Entity
가 있는 클래스의 경우 실제 테이블과 매핑되지만, @MappedSuperclass
를 사용한 클래스는 실제 테이블과 매핑되지 않고, 공통 매핑 정보가 필요할 때 사용할 목적으로만 사용된다.
아래 그림처럼 id, name과 같은 공통 매핑 정보가 필요할 때 BaseEntity
클래스를 만들어서 Member와 Seller로부터 상속받게 설계를 한다.
// BaseEntity
@MappedSuperclass
public abstract class BaseEntity{
@Id @GeneratedValue
private Long id;
private String name;
...
}
// Member 엔티티
@Entity
public class Member exteds BaseEntity{
// ID, NAME 상속
private String email;
...
}
// Seller 엔티티
@Entity
public class Seller exteds BaseEntity{
// ID, NAME 상속
private String shopName;
...
}
@MappedSuperclass
특징을 정리하면 아래와 같다.
상속관계 매핑이 아니다.
그래서 엔티티도 아니고 테이블과 매핑하지도 않는다.
부모 클래스를 상속 받은 자식 클래스에 매핑 정보만 제공해준다.
직접 생성해서 사용할 일이 없기 때문에 추상 클래스로 사용하는 것을 권장한다.
엔티티와 다르게 BaseEntity
추상 클래스에는 조회나 검색이 불가능하다.
정리하면, @MappedSuperclass
는 테이블과 관계 없고, 단순히 엔티티가 공통으로 사용하는 매핑 정보를 모으는 역할이고,
주로 등록일, 수정일, 등록자, 수정자와 같은 전체 엔티티에서 공통적으로 적용하는 정보를 모을 때 사용한다.
굿프렌즈 프로젝트에서 몇몇 테이블에 등록 시간과 수정 시간이 공통적으로 필요해서, @MappedSuperclass
을 사용했다.
회원, 상품, 주문, 신고 테이블에 등록 시간과 수정 시간이 필요해서 BaseTimeEntity
추상 클래스를 만들어서 상속받도록 했다.
프로필, 신고 테이블에는 등록 시간만 필요해서 BaseCreateTimeEntity
추상 클래스를 만들어서 상속받도록 했다.
굿프렌즈
프로젝트에서는 공통으로 쓰는 클래스를 global -> common 패키지에 넣어두었다.
@EntityListeners(AuditingEntityListener.class)
에 대한 부분은 나중에 다른 포스팅에서 설명할 에정이다.BaseTimeEntity - 등록 시간, 수정 시간
```java package woorifisa.goodfriends.backend.global.common;
이 글은 [자바 ORM 표준 JPA 프로그래밍 - 기본편] 강의를 듣고 정리한 내용입니다.
다대일에서 다
쪽이 연관관계 주인이다.
다대일 단방향
은 가장 많이 사용하는 연관관계다.외래 키가 있는 쪽을 연관관계의 주인으로 한다.
양쪽이 서로 참조되도록 개발한다.
일대다에서 일
쪽이 연관관계 주인이다.
일대다 단방향
모델은 권장하지 않는다. (실무에서 거의 쓰이지 않는다)DB 설계상 일대다 관계는 항상 다
쪽에 외래키
가 있다.
객체와 테이블의 차이 때문에 반대편 테이블의 외래 키(Member의 외래 키)를 관리하는 특이한 구조이다.
@JoinColumn
을 꼭 사용해야 한다. 그렇지 않으면 조인 테이블 방식을 사용해야 한다. (@JoinColumn을 사용하지 않으면 TEAM_MEMBER와 같은 중간에 테이블 하나가 추가된다)
Team 엔티티 클래스에 대해 java 코드로 작성하면 아래와 같다.
@Entity
public class Team {
@Id @GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
private String name;
@OneToMany(mappedBy = "team") // Team은 일대다에 '일'에 속하므로 @One으로 시작한다.
@JoinColumn(name ="TEAM_ID") // **
List<Member> members = new ArrayList<Member>();
// 아래 부분 생략
}
일대다 단뱡향 매핑의 단점으로는 아래와 같다.
엔티티가 관리하는 외래 키가 다른 테이블에 있다.
연관관계 관리를 위해 추가로 UPDATE SQL을 실행해줘야 한다.
정리하면, 일대다 단방향 매핑보다는 다대일 양방향
매핑을 사용하자.
일대다 양방향
매핑은 공식적으로 존재하지 않는다.
만약 다
인 Member
테이블에서 조회하고 싶다면 아래와 같이 작성하면 된다.
읽기 전용 필드(@JoinColumn(insertable=false, updatable=false))을 사용해서 양방향 처럼 사용한다.
@Entity
public class Member {
@Id
@GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String name;
private int age;
@ManyToOne
@JoinColumn(name = "TEAM_ID", insertable=false, updatable=false) // 읽기 전용 필드를 사용
private Team team;
// 아래 부분 생략
}
다대일 양방향
매핑을 사용하자.주 테이블이나 대상 테이블 중에 외래 키
를 선택 가능하다.
둘 중 한군데에만 넣어도 상관없다.
외래 키
에 데이터베이스 유니크(UNI) 제약조건을 추가한다.
다대일(@ManyToOne) 단방향 매핑과 유사하다.
@ManyToOne
에서 @OneToOne
으로 바꾼 것 뿐이다.
Member 클래스
@Entity
public class Member {
@Id
@GeneratedValue
private Long id;
@Column(name = "USERNAME")
private String name;
private int age;
@OneToOne
@JoinColumn(name = "LOCKER_ID")
private Locker locker;
// 아래 부분 생략
}
다대일 양방향 매핑 처럼 외래 키가 있는 곳이 연관관계의 주인이다.
주인이 아닌 곳(반대편)은 mappedBy를 적용한다.
Locker 클래스
@Entity
public class Locker {
@Id
@GeneratedValue
private Long id;
private String name;
// 읽기 전용
@OneToOne(mappedBy = "locker") // Member 클래스에 Locker의 변수명을 의미한다.
private Member member;
// 아래 부분 생략
}
관계형 데이터베이스
는 정규화된 테이블 2개로 다대다 관계를 표현할 수 없다.
따라서 연결 테이블을 추가해서 일대다, 다대일 관계로 풀어나가야 한다.
객체
는 컬렉션을 사용해서 객체 2개로 다대다 관계가 가능하다.
@ManyToMany
사용하고, @JoinTable
로 연결 테이블을 지정한다.
다대다 매핑은 단방향, 양방향 모두 가능하다.
결론부터 말하자면, 다대다 매핑는 실무에서 사용하지 않는다.
연결 테이블이 단순히 연결만 하고 끝나지 않는다.
주문시간, 수량 같은 데이터가 들어올 수 있다.
다대다 한계를 극복하기 위해 연결 테이블용 엔티티
를 추가한다. (연결 테이블을 엔티티로 승격)
@ManyToMany
-> @OneToMany
, @ManyToOne
으로 바꿔준다.
@ManyToMany
사용하지 않는다.@JoinColumn
은 외래 키를 매핑할 때 사용하는 어노테이션이다.@ManyToOne
은 다대일 관계 매핑할 때 사용하는 어노테이션이다.
다대일에서 ‘다’가 연관관계 주인이 되어야 한다.
@OneToMany
은 일대다 관계를 매핑할 때 사용하는 어노테이션이다.이 글은 [자바 ORM 표준 JPA 프로그래밍 - 기본편] 강의를 듣고 정리한 내용입니다.
객체지향 설계의 목표는 자율적인 객체들의 협력 공동체를 만드는 것이다 - 조영호(객체지향의 사실과 오해)
이번 글에서는 JPA에서 외래 키
로 조인을 사용하고, 객체
는 참조를 사용해서 연관관계를 매핑하는 방법에 대해서 알아보자.
예를 들어, 회원과 팀이 있고,
회원은 하나의 팀에만 소속될 수 있고,
회원(다)과 팀(일)은 다대일 관계라고 시나리오를 가정해보자.
이때, 객체를 테이블에 맞추어서 모델링을 하면, Member, Team 두개의 테이블을 통해 식별자(member.getId(), team.getId())로 조회해야 한다.
이처럼 객체를 테이블에 맞추어 데이터 중심으로 모델링하면, 협력 관계를 만들 수 없다.
따라서 객체지향 설계로 협력 관계를 만들기 위해 아래와 같이 생각해서 매핑한다.
테이블은 외래 키
로 조인을 사용해서 연관된 테이블을 찾는다.
객체는 참조
를 사용해서 연관된 객체를 찾는다.
객체 지향적인 모델링(객체 연관관계 사용)을 하면 아래 그림과 같다.
@Entity
public class Member {
@Id
@GeneratedValue
private Long id;
@Column(name = "USERNAME")
private String name;
private int age;
// @Column(name ="TEAM_ID")
// private Long teamId;
@ManyToOne // User: M이므로 '다'에 속하기 때문에 Many~로 시작한다.
@JoinColumn(name = "TEAM_ID") // 조인할 테이블의 식별키를 작성한다. => 외래 키
private Team team;
// 아래 Getter 생략
}
//팀 저장
Team team = new Team();
team.setName("TeamA");
em.persist(team);
//회원 저장
Member member = new Member();
member.setName("member1");
member.setTeam(team); //단방향 연관관계 설정, 참조 저장
em.persist(member);
//조회
Member findMember = em.find(Member.class, member.getId());
//참조를 사용해서 연관관계 조회
Team findTeam = findMember.getTeam();
@Entity
public class Member {
@Id
@GeneratedValue
private Long id;
@Column(name = "USERNAME")
private String name;
private int age;
@ManyToOne // Member: M이므로 '다'에 속하기 때문에 Many~로 시작한다.
@JoinColumn(name = "TEAM_ID") // 조인할 테이블의 식별키를 작성한다. => 외래 키
private Team team;
// 아래 부분 생략
}
@Entity
public class Team {
@Id
@GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "team") // Team은 일대다에 '일'에 속하므로 @One으로 시작한다.
List<Member> members = new ArrayList<Member>();
// 아래 부분 생략
}
(mappedBy = "team")
은 Member
엔티티에 있는 Team의 변수명인 team과 연관되었다는 걸 의미한다.객체와 테이블 간에 연관관계를 맺는 차이를 이해해보자.
객체
는 연관관계가 2개이고, 테이블
은 연관관계가 1개로 구성되어 있다.
객체) 회원 -> 팀 연관관계 1개(단방향), 팀 -> 회원 연관관계 1개(단방향)
테이블) 회원 <-> 팀의 연관관계 1개(양방향)
객체의 양방향 관계는 사실 양방향 관계가 아니라 서로 다른 단방향 관계 2개
다.
따라서 객체를 양방향으로 참조하려면 단방향 연관관계를 2개 만들어야 한다.
테이블은 외래 키 하나로 두 테이블의 연관관계를 관리한다.
이때, 둘 중 하나로 외래 키를 관리해야 한다.
양방향 매핑에는 다음과 같은 규칙이 있다.
객체의 두 관계중 하나를 연관관계의 주인
으로 지정해야 한다.
연관관계의 주인만이 외래 키
를 관리(등록, 수정)할 수 있다.
주인
은 mappedBy 속성을 사용하지 않는다.
주인이 아닌쪽
은 읽기만 가능하므로 mappedBy 속성
으로 주인을 지정해준다.
결론부터 말하면, 외래 키
의 위치를 기준으로 연관관계 주인을 정하면 된다. (연관관계 주인 => 외래 키가 있는 곳)
여기서는 Member.team
이 연관관계의 주인이다.
Team team = new Team();
team.setName("TeamA");
em.persist(team);
Member member = new Member();
member.setName("member1");
team.getMembers().add(member);
//연관관계의 주인에 값 설정
member.setTeam(team); //**중요**
em.persist(member);
단방향 매핑만으로도 이미 연관관계 매핑은 완료된다.
양방향 매핑은 반대 방향으로 조회(객체 그래프 탐색) 기능이 추가된 것 뿐이다.
JPQL에서 역방향으로 탐색할 일이 많을 때 사용한다.
정리하면, [1] 처음에는 단방향 매핑으로 잡은 뒤, [2] 필요할 때 양방향으로 추가해준다. (단방향 -> 양방향으로 바꿀 때 테이블에 영향을 주지 않는다.)
이 글은 [자바 ORM 표준 JPA 프로그래밍 - 기본편] 강의를 듣고 정리한 내용입니다.
@Entity
가 붙은 클래스는 JPA가 관리하며, 엔티티
라 부른다.
JPA를 사용해서 테이블과 매핑할 클래스는 @Entity
가 필수이다.
몇 가지 주의점이 있다.
기본 생성자가 필수이다. (파라미터가 없는 public 또는 protected 생성자)
final 클래스, enum 클래스, interface, inner 클래스는 사용할 수 없다.
저장할 필드에 final 사용할 수 없다.
@Entity 속성 정리
속성: name
: JPA에서 사용할 엔티티 이름을 지정한다.
기본값: 클래스 이름을 그대로 사용한다.
@Entity(name = "User")
같은 클래스 이름이 없으면 가급적 기본값을 사용한다.
@Table
은 엔티티와 매핑할 테이블을 지정한다.
속성 1) name
: 클래스 이름이 MySQL 예약어와 겹치면 매핑되는 테이블 이름은 복수형
으로 지정한다.
기본값은 엔티티 이름을 사용한다.
ex) Entity Class 이름 = User -> @Table(name = "users")
속성 2) catalog
: 데이터베이스 catalog를 매핑한다.
속성 3) schema
: 데이터베이스 schema를 매핑한다.
속성 4) uniqueConstraints
: DDL 생성 시에 유니크 제약 조건을 생성한다.
User 클래스(엔티티)
package woorifisa.goodfriends.backend.user.domain;
import woorifisa.goodfriends.backend.global.common.BaseTimeEntity;
import woorifisa.goodfriends.backend.user.exception.InvalidUserException;
import javax.persistence.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Table(name = "users")
@Entity
public class User extends BaseTimeEntity {
private static final Pattern EMAIL_PATTERN = Pattern.compile("^[a-z0-9._-]+@[a-z]+[.]+[a-z]{2,3}$");
private static final int MAX_NICKNAME_LENGTH = 20;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "user_id")
private Long id;
@Column(name = "email", nullable = false)
private String email;
@Column(name = "nickname", nullable = false)
private String nickname;
@Column(name = "profile_image_url", nullable = false)
private String profileImageUrl;
protected User() {
}
public User(String email, String nickname, String profileImageUrl) {
validateEmail(email);
validateNickname(nickname);
this.email = email;
this.nickname = nickname;
this.profileImageUrl = profileImageUrl;
}
private void validateEmail(final String email) {
Matcher matcher = EMAIL_PATTERN.matcher(email);
if (!matcher.matches()) {
throw new InvalidUserException();
}
}
private void validateNickname(final String nickname) {
if(nickname.isEmpty() || nickname.length() > MAX_NICKNAME_LENGTH) {
throw new InvalidUserException(String.format("이름은 1자 이상 %d자 이하여야 합니다.", MAX_NICKNAME_LENGTH));
}
}
// getter
}
DDL이란 데이터베이스를 정의하는 언어이며, 데이터를 생성, 수정, 삭제하는 등의 데이터 전체의 골격을 결정하는 역할을 하는 언어이다.
테이블 관련 명령어 : create(생성), alter(수정), drop(삭제), truncate(초기화)
DDL을 애플리케이션 실행 시점에 자동으로 생성해준다. (테이블 -> 객체 중심)
데이터베이스 방언
(DB Dialect) 을 활용해서 데이터베이스에 적절한 DDL을 생성한다.
Hibernate의 경우엔 persistence.xml
에서 hibernate.dialect 설정 값을 변경하면 된다.
예를 들어, H2 Dialect 설정 코드는 <property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect" />
이다.
```markdown
SQL은 다음과 같이 표준 SQL인 ANSI SQL이 있으며, 이외에 각 DBMS Vendor(벤더, 공급업체)인 MS-SQL, Oracle, MySQL, PostgreSQL 에서 자신만의 기능을 추가한 SQL이 있다.