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” 까지만 따져서 문제였음
  • 해결하기
    1. 똑같이 /user 처럼 두번째 슬래시 전까지 파싱해서 ControllerPool에 등록(@RequestMapping이 있는 클래스)된 Controller의 @RequestMapping value()와 비교
    2. 첫번째 단계에서 아무것도 매칭되지않는다면 바로 ViewController를 리턴함 : ViewController에서 처리할 때 해당 리소스가 없으면 404 응답코드를 담은 메세지 응답
    3. 첫번째 단계에서 매칭되었다면 상세 맵핑을 해봄 - 메소드에도 @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