해결된 질문
작성
·
870
답변 9
3
친절한 질문 보충 감사합니다^^!
이번 케이스의 해결방안은 모두 장단점이 있어서 제가 아는 한도에서 열심히 설명드리겠습니다^^
1. arc를 유지하는 방안
JPA는 연관관계를 매핑할 때 명확하게 타입을 지정해야 합니다. 그래서 연관관계를 매핑하는 방법으로는 해결책이 없습니다.
해결방안: JPA 연관관계를 사용하지 않고, 단순히 Point 엔티티에 int target_id 필드를 유지하면 됩니다.
의견: arc를 유지하는 것은 하나의 필드가 두가지 의미를 가지므로, 유지보수 관점에서 저는 권장하지 않는 스타일 입니다.
2. arc를 유지하지 않는 방안
적립 타입에 따라서 관리하는 데이터가 다르기 때문에 상속관계 매핑을 사용해서 문제를 해결할 수 있습니다.
Point 엔티티의 공통 부분은 유지하고, Point 적립 타입에 따라서 상속 타입을 분류합니다.
(테이블 관점에서는 Point 테이블을 슈퍼타입 서브타입 논리 모델로 만듭니다.)
대략적인 코드를 작성하면 다음과 같습니다.
Point {
공통 속성 존재
}
MemberPoint extends Point {
Member target_member_id;
}
OfferWallPoint Point {
OfferWall target_offer_wall;
}
이렇게 객체는 상속 관계 매핑을 사용하고, 테이블은 조인 전략(JOIND)을 사용할지, 아니면 단일 테이블 전략(SINGLE_TABLE)을 사용할지 둘중에 하나를 선택하면 됩니다.
이렇게 하면 arc를 유지하지 않으면서, type에 따라 다른 필드들을 정규화(조인 전략 사용시)해서 저장할 수 있습니다.
의견: 이렇게 했을 때 객체와 테이블을 다 정규화 할 수 있다는 장점이 있습니다. 추가로 기능이 확장되어서 포인트 적립 타입을 향후 추가해야 할 때, 객체 입장에서 단순히 상속 타입을 하나 추가해주면 되는 장점이 있습니다. 이 방법의 단점은 상속관계를 사용하기 때문에 시스템 복잡도가 높아진다는 점입니다.
자세한 내용은 상속관계 매핑 부분을 참고해주세요. :)
제가 생각할 때 복잡한 상속 대신에 단순한 방향으로 선택할 수 있는 트레이드 오프는, 다음과 같이 필드를 단순하게 둘로 나누고, 타입에 따라서 적절하게 선택하는 것도 나쁘지 않은 방법이라 생각합니다.(물론 이 방식은 단순하지만 한쪽에 null이 입력되는 단점이 있습니다.)
Point {
공통 속성 존재
Type type; 적립 타입
Member target_member; //null 허용
OfferWall target_offer_wall; //null 허용
}
몇가지를 쭉 말씀드렸는데, 다 장단점을 가지고 있어서, 명확하게 이것을 딱 선택하세요 라고 말하기는 어렵네요. 이론적으로는 상속관계 매핑을 활용하는게 가장 나은 선택 같지만, 실무에서는 복잡도가 높아져서 고민이되겠네요.
정리하면 앞으로 이 기능이 자주 확장될 것 같다라고 하면 상속관계를 고려해보는 것도 좋고, 그게 아니라 이 기능이 거의 확장이 안될 것 같다 하면, arc를 유지하거나, 필드를 둘로 쪼개서 관리하거나 하는 것도 실용적인 관점에서 좋다 생각합니다.
혹시 더 나은 의견이 있거나 더해주실 내용이 있으면 공유부탁드려요^^
1
어이쿠 너무 칭찬해주셔서 감사하고, 부끄럽습니다. ㅎㅎ
JPA라는 것이 사실 특별한 기술이라기 보다는 관계형 DB를 사용하는 환경에서도, 객체지향 설계가 가능하도록 도와주는 것이 핵심입니다. 결국 객체지향 설계라는 본질에 대해서 깊이있게 고민하고, 학습하시면 더 좋은 결과가 있을 거라 기대합니다.
그리고 JPA가 아무리 좋아도, 프로젝트는 보통 팀으로 하는 것이기 때문에, 팀에 최소 두분 정도는 JPA를 다룰 수 있는 분이 있어야 합니다. 팀 분들을 잘 설득하고, 필요하면 함께 스터디도 하면서 기술력을 쌓아서, 새로운 작은 프로젝트 부터 조금씩 성공과 자신감을 얻는 것이 중요하다 생각합니다.
너무 급하게 가기 보다는 한발한발 고민하면서, 진행하시면 좋은 결과가 있을 것이라 확신합니다^^
즐거운 하루 되시고, 궁금한 내용은 종종 문의주셔도 됩니다. ㅎㅎ
1
안녕하세요 Heeseon O 님^^
우선 코드를 드릴께요.
@Entity
public class Member {
@Id @GeneratedValue
private Long memberId;
private String name;
}
@Entity
public class OfferWall {
@Id @GeneratedValue
private Long offerWallId;
private String adsName;
}
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
public class Point {
@Id @GeneratedValue
private Long pointId;
@ManyToOne
@JoinColumn(name = "member_id")
private Member member;
private int value;
}
@Entity
public class RecommendPoint extends Point {
@ManyToOne
@JoinColumn(name = "target_member_id")
private Member recommendMember;
}
@Entity
public class OfferWallPoint extends Point {
@ManyToOne
@JoinColumn(name = "offer_wall_id")
private OfferWall offerWall;
}
JPA의 상속관계 매핑을 사용했고, 슈퍼타입 서브타입 전략은 JOIND를 사용했습니다.
(JOIND를 사용하면 POINT, OFFER_WALL_POINT, RECOMMEND_POINT 이렇게 테이블이 생성되고, pointId PK를 모두 가지고 이 값으로 연결됩니다.)
여기서 중요한 포인트는 target_id를 제거하고, 대신에 RecommendPoint, OfferWallPoint라는 명확한 하위 타입을 만들었습니다. 그리고 필요한 곳에서 recommandMember, offerWall 참조를 사용하도록 변경했습니다. (아마 이게 궁금하셨을 거에요)
회원 추천으로 저장해야 하면, RecommendPoint를 저장하면 되고,
만약 광고 추천으로 저장해야 하면, OfferWallPoint를 저장하면 됩니다.
(OfferWall이 어떤 것을 의도하는지 제가 정확히 몰라서, 그냥 광고라고 생각했습니다. 해당 광고를 통해서 들어왔기 때문에 OfferWallPoint를 저장할 때 해당 광고의 참조값을 저장하도록 했습니다.)
조회할 때도 전체를 조회하고 싶으면 Point 타입으로 조회하면 되고, 만약 RecommandPoint 만 조회하고 싶으면, 해당 타입으로 조회하면 됩니다.
arc 여부를 떠나서 포인트를 적립하는 타입마다, 저장해야 하는 데이터가 다르기 때문에, 만약 제가 설계를 했다면 설명드린 것 처럼 설계를 했을거에요. 향후 다른 포인트 적립 기준을 추가할 때도 확장성 면에서 유리합니다. (실제로 현업에서 이렇게 진행한 적도 있습니다. 물론 상황에 따라 이것이 정답인 것은 아닙니다. 결국 설계라는 것이 트레이드 오프가 있으니까요.)
중요한 것은 JPA가 이런 상속관계에 대해서 많은 부분을 자동화 해서 처리해 주기 때문에, 얻는 이점이 많습니다. 그래서 사용하는 것이지 만약! JPA를 사용하지 않고, 직접 SQL을 작성해야 하면 이런 상속관계를 구성하는 순간 개발 코드와 SQL을 어마어마하게 수동으로 작성해야 합니다. 이렇게 SQL을 직접 처리해야 하면 차라리 arc 관계를 사용하는게 실용적인 관점에서 더 나은 선택일 수 있습니다.
드린 코드를 기반으로 JPA 상속관계를 다양하게 연습해보시면 감이 오실꺼에요^^
그리고 코드를 드렸으니 테이블 DDL은 코드를 실행해서 뽑아보시면 될꺼에요^^
2번 질문 point_manager_id를 사용하고 안하고는 사실 그렇게 중요하지 않습니다. 이미 객체의 타입이 OfferWallPoint인지, RecommendPoint인지로 구분하기 때문에 point_manager_id를 계속 사용할지 안할지는 옵션으로 남습니다.
1,3번 질문은 보내드린 코드를 기반으로 테스트 해보시고 JPA 상속관계를 공부해보시면 정리가 되실 거라 생각합니다^^ (실제 데이터를 넣고 빼고 돌려보면서요^^!)
감사합니다.
1
ㅎㅎ 네 최대한 깊이있게 파보시고!
그래도 막히면 언제든지 문의주세요^^
(상속관계 매핑이 꼭 편하다는 뜻은 아닙니다 ㅎㅎ 한번 쭉 깊이 있게 공부하고 고민해보시면 지금 현 상황에 맞는 적절한 방안을 잘 선택하실꺼라 생각합니다^^!)
1
실제 업무에서는 현재 자주 사용 되는 것만 해도 4개까지 확장 되어 있고, 자주 사용 되지 않는것까지 포함하면 10개가 넘어 갑니다.
저는 JPA도 적용 시키고 싶고, 유지보수도 편한걸 좋아하니 김영한님의 조언을 참고하여, 상속관계를 매핑하여 해결 하는 방향으로 가보도록 하겠습니다.
먼저 Point 테이블을 슈퍼타입과 서브타입으로 만들라는 말씀이 지금은 명확하게 어떤 뜻인지 이해 하지 못했지만 더 공부하여 시도해 보고, 혹시 막히는 부분이 있다면 그 때 김영한님의 조언을 다시 한 번 부탁드리겠습니다.(최대한 질문하지 않도록 하겠습니다.)
늦은 시간까지 정성스러운 답변 감사드립니다.ㅠ
좋은 밤 보내시길 바랍니다. ^^
1
안녕하세요 김영한님 :D
늦은 시간까지 고생이 많으십니다.
제가 아크 관계에 여러 케이스가 있는지 몰랐습니다ㅠㅠ
비효율적인 질문을 하여 두 번 답변 하시게 만들어 송구스럽습니다.ㅠㅠ
먼저 ERD입니다.
최대한 자세하고, 간단하게 작성했습니다.
업무 내용은 포인트 적립입니다.
예제에서는 포인트를 적립하는 방법은
1. 광고를 보는 것과
2. 다른 사람을 초대하여 회원 가입 시키는 것입니다.
`point` 테이블에서 주요 내용은
- 누구에게 적립 시켜줄 것인지 (`member_id`)
- 어떤 유형의 포인트 적립인지 (`point_manager_id`)
- 이 값에 따라 target_id가 참조해야 할 테이블이 바뀝니다.
- 적립 되는 소스를 가리키는 (`target_id`)가 있습니다.
- 이 `target_id`의 참조 대상은 `point_manager_id`의 값에 따라 `member.id` 일수도 있고,
`offer_wall.id` 일수도 있습니다.
- 참조해야 할 대상이 없는 경우 이 값은 `0`으로 들어갑니다. 사실상 `null`이죠.
ex) 출석체크. 출석체크 관련 테이블은 없습니다.
예제에서는 `1`이면 `member` 테이블을 참조하고, `2`이면 `offer_wall` 테이블을 참조한다고 보시면 되겠습니다.
아래 테이블 DDL과 INSERT용 DML을 작성 하였습니다.
`mariadb`를 사용하였고 스키마 이름은 `test`로 생성 하였습니다.
/* 회원 정보 */
create table member
(
id int auto_increment
primary key,
recommender int null comment '추천인 회원 번호'
);
/* 광고 정보 */
create table offerwall
(
id int auto_increment
primary key,
ads_name varchar(45) not null comment '광고 내용'
);
/* 포인트 적립 내역 */
create table point
(
id int auto_increment
primary key,
member_id int not null comment '회원 번호',
point_manager_id int not null comment '포인트 분류 번호',
target_id int default 0 not null comment '회원 번호 or 광고 번호',
content varchar(50) not null comment '포인트 적립 내용',
value int not null comment '적립 금액',
constraint point_member_id_fk
foreign key (member_id) references member (id),
constraint point_offerwall_id_fk
foreign key (target_id) references offerwall (id),
constraint point_member_id_fk_2
foreign key (target_id) references member (id),
constraint point_point_manager_id_fk
foreign key (point_manager_id) references point_manager (id)
)
charset = utf8;
/* 포인트 적립 구분 */
create table point_manager
(
id int auto_increment
primary key,
content varchar(25) not null
)
charset = utf8;
/* INSERT member */
INSERT INTO test.member (recommender) VALUES (null);
INSERT INTO test.member (recommender) VALUES (1);
/* INSERT ads */
INSERT INTO test.offerwall (id, ads_name) VALUES (1, 'JPA ADS');
INSERT INTO test.offerwall (id, ads_name) VALUES (2, 'JAVA ADS');
/* INSERT point */
INSERT INTO test.point (id, member_id, point_manager_id, target_id, content, value) VALUES (1, 1, 1, 2, '친구 초대', 10);
INSERT INTO test.point (id, member_id, point_manager_id, target_id, content, value) VALUES (2, 1, 2, 1, 'JPA ADS', 1);
INSERT INTO test.point (id, member_id, point_manager_id, target_id, content, value) VALUES (3, 2, 2, 2, 'JAVA ADS', 1);
/* INSERT point_manager */
INSERT INTO test.point_manager (id, content) VALUES (1, '친구 초대');
INSERT INTO test.point_manager (id, content) VALUES (2, '광고 적립');
혹시 이 구조에서 아크 관계가 아닌 다른 방식을 추천 하신다면 어디를 어떻게 변경하는게 좋은지 알려주시면 감사하겠습니다.
아니라면 아크 관계에서도 JPA를 잘 적용하여 사용할 수 있는 방법이 있으면 어떤 방식으로 해결 할 수 있는지 알려주시면 감사하겠습니다. (_ _)
p.s: 토비의 스프링은 오래 전부터 회사 모니터 아래에서 더할나위 없이 확실하게 제 역할을 해내는 중입니다. 특히 듀얼모니터와는 최고의 시너지를 발휘하는 것 같아 다른 제품을 고려해볼 필요도 없는 최고의 물건이더군요. 김영한님께서 추천해주시는 말씀을 들으니 역시 제 선택이 틀리지 않았다는 확신을 다시 한 번 하게 됩니다.
0
김영한님의 따듯한 답변에 온몸이 녹아버릴 것 같습니다..ㅠㅠ
늦은 밤에도 이렇게 정성스런 답변을 해주시다니, 천사가 사람의 흉내를 내고 있는지 의심스러운 부분입니다.
JPA를 공부하기 위해 이 강의를 선택한건 신의 한수였던것 같습니다. (널리 알리겠습니다.)
그 동안 김영한님께서 주신 답변이 제가 JPA를 공부하여 실무에 적용하는데 정말 큰 도움이 될 것 같습니다.
이런 고퀄리티의 답변을 주실지 상상도 못했습니다.
정말 너무 감사드리고, 앞으로도 김영한님의 모든 강의와 도서를 다 구매하여 작게나마 은혜를 보답하겠습니다.
이후에는 질문이 아닌 실무에 JPA를 완전히 적용 했을 때 어떻게 적용했는지에 대한 후기를 남기겠습니다.
실무에 언제 적용이 가능할지 아직은 알수 없지만, 확실한건 적용하게 되면 그건 김영한님의 강의와 답변이 없었다면 매우 힘든 일이였을 것입니다.
다시 한 번 정성스러운 답변에 감사드리며, 그 모습에 존경 합니다. (_ _)
0
안녕하세요 김영한님 제가 어디 물어볼 곳이 없어 다시 한번 여쭙게 되네요.(하루만에 다시 찾아온 이 무능력한 사람..)
바쁘실텐데 자꾸 죄송합니다.
세 가지 확인 하고 싶은 부분이 있어 질문 드립니다. 답변 해주시면 감사하겠습니다.ㅠㅠ
1. arc를 유지하지 않는 방안이라면 `point` 테이블에서 `target_id`를 제거 하고, point 테이블을 슈퍼타입 테이블로 공통 값을 남겨두고, 나머지는 구분 타입마다 서브 테이블을 하나씩 만든다는 말씀 인가요?
[기존 ERD]
[신규 ERD]
`point` 테이블에서 `target_id`를 제거 하고 공통 부분은 남겨 둔 채, 타입별로 서브 테이블을 만들었습니다.
`member`(회원 정보) 테이블에 `point_id`를 만들려니 뭔가 이상한 것 같아서 추천인 회원 가입 테이블 `recommender`를 서브 테이블로 생성 하였습니다.
`offer_wall` 테이블은 기존 테이블에서 `point_id`만 추가하면 서브 테이블이 될것 같아서 따로 테이블을 생성하지 않았습니다.
이렇게 타입별로 서브테이블이 생성 되는 것이 맞는 건가요?
2. 여전히 `point_manager_id`로 테이블을 구분하여 `point_id`를 통해 아크 타입으로 하위 테이블로 찾아가는게 가능합니다.
제가 잘못 만들어서 아직 아크 관계처럼 동작이 가능한건가요?
3. 마지막으로 가장 궁금한 질문입니다.
전체 데이터(모든타입)에서 `point_id`가 유니크한 값이 됐기 때문에 서브 테이블에서 선처리를 하고, 슈퍼 테이블로 올라갈 수 있게 된거 같습니다.
슈퍼/서브 테이블 구조에 JPA를 적용하게 되면 아직 아크 관계처럼 동작할 수 있는 방법은 지양 하고, 항상 서브 테이블에서 선처리를 해야 되는건가요?
0
안녕하세요 Heeseon O님^^
어려운 질문을 주셨군요~ ㅎㅎ
제가 질문에 더 좋은 답변을 드리고 싶어서 그런데, 다음 내용을 채워주세요.
- 구체적인 예시를 ERD에 그려서(종이에 그려도 됩니다) 매우 구체적으로 설명해주세요.
- 예시용 테이블 DDL을 정확하게 적어주세요. (제약조건 모두 포함)
- 예시가 동작할 수 있도록 INSERT용 DML을 정확하게 적어주세요.
P.s: 모니터 받침으로 자매품 토비의 스프링도 추천합니다. ㅎㅎ
그럼 추가 질문 기다릴께요^^