TIL - 0713
JPA
연관관계 맵핑 - 객체지향과 데이터베이스의 패러다임 불일치 인식하기
- 객체의 관계는 단방향이지만, 데이터베이스의 테이블은 fk 하나로 양방향 관계를 맺을 수 있음
- 반대쪽에 객체 참조 필드를 생성하기 전까지 단방향 관계임 : 테이블은 SQL FK로 JOIN을 사용하면 둘다 조회가능하지만 객체는 .get()과 같은 메소드를 사용해야하는데 참조 필드가 없으면 할 수가 없음(패러다임의 불일치) - .get()을 해야 내부적으로 SQL을 생성할텐데 없으면 그렇지도 못함(이 부분은 없애고 올리기)
/****** 단방향 ******/
public class Member {
private Long id;
private Team team;
}
public class Team {
private Long id;
}
/****** 양방향 ******/
public class Member {
private Team team;
}
public class Team {
private List<Member> members;
}
/* SQL JOIN */
SELECT * FROM MEMBER M JOIN TEAM T ON M.TEAM_ID = T.ID
SELECT * FROM TEAM T JOIN MEMBER M ON T.TEAM_ID = M.TEAM_ID
연관관계 - 방향과 다중성
- 방향 : 단방향, 양방향 - 한 쪽에서만 조회되게할 것인지, 양 쪽 다 조회되게 할 것인지
- 객체지향에서 양방향 조회되게 만들려면 둘 다 참조변수를 만들어줘야하고, 관계 변화 시 둘 다 변화가 적용되도록 잘 관리(로직 작성 시 견고하게 - 아래(마지막)에서 살펴봄)해야함
- 다중성 : N:1, 1:N, M:N, 1:1 어떤 관계를 가지게되는지
연관관계 맵핑하기 - 단방향(Member -> Team), N:1
- 여러 Member는 Team 1개에 연관될 수 있고, Member만 Team 참조 필드를 가지고 있으므로 단방향
- 객체그래프 탐색 : member.getTeam()과 member라는 참조로 다른 객체를 찾는 것을 객체그래프 탐색이라고 함(다른 객체를 불러오는 것 - SQL이 생성되고 실행되겠지)
- 아래는 JPA가 생성해내는 DDL
public class MappingTest {
@Test
public void mapping() {
Member member1 = new Member(1L, "colin");
Member member2 = new Member(2L, "jinbro");
Team team = new Team(1L, "team1");
member1.setTeam(team);
member2.setTeam(team);
Team findTeam = member1.getTeam();
assertEquals(team, findTeam);
}
}
CREATE TABLE MEMBER (
MEMBER_ID VARCHAR(255) NOT NULL,
TEAM_ID VARCHAR(255),
NAME VARCHAR(255),
PRIMARY KEY (MEMBER_ID)
)
CREATE TABLE TEAM (
TEAM_ID VARCHAR(255) NOT NULL,
NAME VARCHAR(255),
PRIMARY KEY (TEAM_ID)
)
ALTER TABLE MEMBER ADD CONSTRAINT FK_MEMBER_TEAM FORIEGN KEY (TEAM_ID) REFERENCES TEAM
.
.
INSERT INTO MEMBER(MEMBER_ID, TEAM_ID, USERNAME) VALUES (1, 1, 'colin'); // member1.setTeam(team);
SELECT * FROM MEMBER M JOIN TEAM T ON M.TEAM_ID = T.ID WHERE M.MEMBER_ID = 1; // member1.getTeam();
- 아래는 JPA를 사용해서 엔티티 정의하기
- 방향은 참조 필드를 어떻게 두냐에 따라 자동 설정됨
- 다중성은 JPA가 제공하는 어노테이션을 설정해두면 됨
@ManyToOne
: Member와 Team 관계를 설정(선언)하는 어노테이션@JoinColumn
: Member.team과 MEMBER.TEAM_ID(객체 참조와 데이터베이스 테이블 외래키를 맵핑), 관계를 맵핑하는 것도 있지만 객체지향과 데이터베이스 패러다임의 차이로 인해 발생하는 부분을 없애기위한 맵핑도 필요함(자동으로 DDL을 생성함)
@Entity
public class Member {
@Id
@Column(name = "MEMBER_ID")
private Long id;
private String name;
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
public Member setTeam(Team team) {
this.team = team;
return this;
}
public Team getTeam() {
return team;
}
}
@Entity
public class Team {
@Id
@Column(name = "TEAM_ID")
private Long id;
private String name;
}
CREATE TABLE MEMBER (
MEMBER_ID VARCHAR(255) NOT NULL,
TEAM_ID VARCHAR(255), // @JoinColumn(name = "TEAM_ID")
NAME VARCHAR(255),
PRIMARY KEY (MEMBER_ID)
)
ALTER TABLE MEMBER ADD CONSTRAINT FK_MEMBER_TEAM FORIEGN KEY (TEAM_ID) REFERENCES TEAM // @JoinColumn(name = "TEAM_ID")
연관관계 맵핑 시 주의할 점
- 연관관계 맵핑 후 Team을 설정할 때 Team은 영속상태여야함
- Id값이 할당되어있고, JPA를 통해서 데이터베이스에서 데이터를 갱신하는 경우 - 외래키(엔티티의 pk)를 저장해야하므로
Team team = new Team(1, "team1");
member1.setTeam(team);
INSERT INTO TEAM (TEAM_ID, NAME) VALUES (1, 'team1');
INSERT INTO MEMBER (MEMBER_ID, NAME, TEAM_ID) VALUES (1, 'colin', 1); // TEAM_ID에 위에서 INSERT로 생성한 TEAM의 ID가 입력됨
- Team을 삭제하려면 우선 Member의 연관관계를 먼저 끊어야함
- Team을 먼저 삭제하면 외래키에 해당하는 Team이 존재하지않으므로 데이터베이스 오류 : cascade 설정해도 됨
member1.setTeam(null);
teamRepository.delete(team1);
맵핑되어있는 엔티티 검색(조회)하기
- 객체그래프 탐색 : 연관관계를 사용해서 조회 - 참조 필드를 통한 조회
- team_id를 필드로 가지고 있었다면 myTeam 코드를 사용하기위해 조회하는 코드를 계속해서 생성해줘야함 : 객체.메소드만 해줘도 될 코드를…
Team myTeam = member1.getTeam();
myTeam.getName();
- JPQL 사용하기 : 알아볼 예정
연관관계 맵핑하기 - 양방향(Member -> Team, Team -> Member), N:1 / 1:N
- 각각 참조 필드를 생성해줌으로서 Team에서도 Member를 조회할 수 있게됨 : 객체그래프 탐색
- 모두에서 조회할 수 있도록 할 때 양방향 연관관계 맵핑을 함 : 변경되는 부분은 Team 엔티티 클래스
@OneToMany
: 1:N 관계를 설정할 때 사용하는 어노테이션, 1(Team):N(Member)mappedBy 속성
: 양방향 관계를 설정할 때 설정해야하는 속성, 반대쪽(Member) 필드의 이름을 넣어주면 됨 - 어디에 맵핑되는지 설정하는 것- 콜렉션(List, Set, Map)이라도 맵핑이 됨 :
타입파라미터로 추론할 수 있음, 타입파라미터가 지정되어있지않다면(Object) 안됨
@Entity
public class Team {
@Id
@Column(name = "TEAM_ID")
private Long id;
private String name;
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
}
mappedBy
- 연관관계의 주인이 됨 : 외래키 관리자를 정하는 것
- N:1 - 1:N 관계에서는 N쪽에서 관리를 함 : Member.team(외래키)를 가지고 있는데, Team이 주인이 된다면 외래키는 Member에 있고 주인은 Team이라 별도의 테이블을 만들어 관리해야하기때문에
- 주인만이 외래키를 등록, 수정할 수 있고, 주인 아닌 쪽은 반대쪽 조회만 가능함 : 주인이 아닌 쪽에서 외래키를 수정하려고 해봤자 수정되지않음, 그렇지만 양쪽에서 모두 조회를 하기이해서 객체 관점에서 List에 add 시켜줘야함(조회 가능해진다는게 객체 관점에서 조회가능하다는 것 - 참조 필드를 만든 것, 조회되려면 Member 객체의 주소값이 저장되어있어야함), 둘 다 설정해주는 것을 까먹지 않기위해서 하나의 메소드로 만들면 됨(주의할 점은 member의 team을 변경할 때 관계를 끊어줘야한다는 것 - 끊지않으면 member는 끊기겠지만 team에서는 그대로 참조를 List에 가지고 있음, 객체 관점으로 그대로 쓰면 됨 양쪽 다 끊어주기, 단순히 참조 필드를 하나 더 만들어두고 조회가 가능해졌다는 점이 달라진 것) - 결국 코딩할 때 잘해야한다는 것, 객체는 단방향을 2개로 양방향처럼 보이게끔 만들어한다는 것(한 쪽은 자동으로 되지만 한 쪽은 만들어줘야함), 데이터베이스는 외래키 하나로 둘다 되지만….(패러다임의 불일치)
team.getMembers().add(member1); // member1에서 getTeam()으로 조회했을 때 TEAM_ID는 변화가 없음
@Entity
public class Member {
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
public Member setTeam(Team team) {
if (Objects.nonNull(this.team)) {
this.team.removeMember(this); // 이미 team이 존재할 때 Member에서만 관계를 끊지않고, Team에서도 끊어줘야함
}
this.team = team;
team.addMember(this);
return this;
}
}
public class JpaMappingTest {
private static final Logger log = LoggerFactory.getLogger(JpaMappingTest.class);
@Autowired
private MemberRepository memberRepository;
@Autowired
private TeamRepository teamRepository;
private Team team;
private Member member;
@Before
public void setUp() throws Exception {
team = new Team(1L, "team1");
member = new Member(1L, "colin");
}
@Test
@Transactional
public void mapping() {
team = teamRepository.save(team);
member = memberRepository.save(member);
memberRepository.save(member.setTeam(team));
log.debug("team id : {}", member.getTeam().getId()); // 1
log.debug("member size : {}", team.getMembers().size()); // 1
memberRepository.save(member.setTeam(team2));
log.debug("team id : {}", member.getTeam().getId()); // 2
log.debug("member size : {}", team.getMembers().size()); // 0
}
}
SQL
데이터베이스
- 데이터의 집합
데이터 베이스와 DBMS
- 데이터베이스 : 데이터의 집합
- 데이터 관리 소프트웨어와 관리 공간(저장 장치)으로 구성
- 저장 공간 : 휘발성 메모리(RAM)에 관리한다면 프로세스 종료나 전원 차단 시 데이터를 영구적으로 보관할 수 없기때문에 데이터를 영구적으로 저장하는 공간(RAM에 비해 속도는 느리지만 싸고 저장공간도 큼, 하드디스크와 SSD 등)을 사용해서 관리함
- DBMS : 데이터베이스 관리 소프트웨어(시스템), 관련 작업을 할 수 있도록 제공, 최적화와 신뢰도를 높이기위한 작업이 되어있음
- 클라이언트/서버 모델로 운영됨 : 다수의 요청을 받을 수 있음(일반 유저에 공개하지않고 WAS에서 대리로 DBMS에 요청하는 방식으로 통신을 함 - 보안을 위해서)
- RDBMS : 테이블로 데이터를 관리하는 시스템 - 행(column), 열(row), mysql/oracle/postgresql 등이 있음
- SQL : 데이터에 대한 작업(RDMBS 작업)을 하기위한 언어
SQL
- RDBMS에서 데이터 관련 작업을 하기위한 언어
- 표준이 있고, RDBMS마다의 자체 SQL 문법이 있음 : 표준으로 거의 커버가 되지만 각각의 SQL이 다른 부분도 있다는 점도 알아야함 - 방언(dialect)이라고 함, 방언 대신 표준을 쓰도록 해야함
- 3가지 종류
- DDL : 데이터 관리자를 만드는 것
- DML : 데이터 조작 - SELECT, INSERT, UPDATE, DELETE
- DCL : 데이터 제어 - 트랜잭션, 접근 제한
java-ims
문제점 수정하기
- HandlerMapper - ControllerPool 기능 문제 : /user/form.html과 /user/create 둘 다 UserController가 맵핑됨
- 생각해보니 디테일하게 따져보지않고 “/user” 까지만 따져서 문제였음
- 해결하기
- 똑같이 /user 처럼 두번째 슬래시 전까지 파싱해서 ControllerPool에 등록(@RequestMapping이 있는 클래스)된 Controller의 @RequestMapping value()와 비교
- 첫번째 단계에서 아무것도 매칭되지않는다면 바로 ViewController를 리턴함 : ViewController에서 처리할 때 해당 리소스가 없으면 404 응답코드를 담은 메세지 응답
- 첫번째 단계에서 매칭되었다면 상세 맵핑을 해봄 - 메소드에도 @RequestMapping 되어있어야 액션 메소드로 인식함 -> Controller의 @RequestMapping value와 Method의 @RequestMapping value와 합쳐서 요청 URI(우선 쿼리스트링 “?” 이전까지 파싱 - GET일 때만 그러도록 변경해야함)와 비교해서 일치하면 Controller 리턴, 아니라면 ViewController 리턴
public class ControllerPool<T extends Controller> {
private static final Logger log = LoggerFactory.getLogger(ControllerPool.class);
private static ControllerPool container = new ControllerPool();
private final Class<T> viewControllerClass = (Class<T>) ViewController.class;
private List<Class<T>> controllers = ImmutableList.of((Class<T>) UserController.class);
private final Map<Class<T>, T> pool;
private ControllerPool() {
pool = new HashMap<>();
controllers.forEach(controllerClass -> pool.put(controllerClass, null));
}
public static ControllerPool of() {
return container;
}
public T search(String requestPath) {
String searchUri = parseFirstUri(requestPath);
Optional<Class<T>> maybeControllerClass = controllers.stream().filter(clazz -> clazz.isAnnotationPresent(RequestMapping.class)).filter(clazz -> clazz.getAnnotation(RequestMapping.class).value().contains(searchUri)).findFirst();
if (!maybeControllerClass.isPresent()) {
return get(viewControllerClass);
}
Class<T> controllerClass = maybeControllerClass.get();
String excludeParams = parseExcludeParams(requestPath);
boolean isMatch = Arrays.stream(controllerClass.getDeclaredMethods())
.filter(method -> method.isAnnotationPresent(RequestMapping.class))
.map(method -> method.getAnnotation(RequestMapping.class))
.anyMatch(annotation -> String.valueOf(controllerClass.getAnnotation(RequestMapping.class).value() + annotation.value()).equals(excludeParams));
return isMatch ? get(controllerClass) : get(viewControllerClass);
}
private T get(Class<T> controllerClass) {
T controller = pool.get(controllerClass);
if (Objects.isNull(controller)) {
return create(controllerClass);
}
return controller;
}
private T create(Class<T> controllerClass) {
try {
Constructor<T> constructor = controllerClass.getDeclaredConstructor();
constructor.setAccessible(true);
T controller = constructor.newInstance();
constructor.setAccessible(false);
pool.put(controllerClass, controller);
return controller;
} catch (InvocationTargetException | NoSuchMethodException | IllegalAccessException | InstantiationException e) {
log.error(e.getMessage());
System.exit(1);
return null;
}
}
private String parseFirstUri(String requestPath) {
int secondSlash = requestPath.replaceFirst("/", "").indexOf("/") + 1;
if (secondSlash == 0) {
return requestPath;
}
return requestPath.substring(0, secondSlash).toLowerCase();
}
private String parseExcludeParams(String requestPath) {
return requestPath.split("\\?")[0];
}
}
- 또다시 수정해야할 부분이라고 생각되는 것
- 컨트롤러의 메소드(interface Controller의 추상메소드를 구현한 메소드)가 고정되어있음 : @RequestMapping만 달리면 컨트롤러의 메소드로 간주할 수 있도록 만들면 어떨까?
- 어찌되었건…. 이번 과정을 통해 리플렉션 API를 왜그렇게 쓰는가가 이해가 됨…. 런타임에 정보를 얻어오려면 어쩔 수가 없네…! 참 좋은 API