03. JPA Entity Class와 패키지 구조


본격적인 개발에 앞서 다음과 같이 패키지 구조를 잡았다.


dao
DAO에 해당하는 클래스를 모아 놓을 것이다. 예를 들어 JpaRepository 나 QueryDSL 관련 클래스는 물론 myBatis Mapper 등이 해당한다.

entity
JPA Entity들이 여기에 해당하며, 본인은 영역을 나누기 위해 com(공통)이라는 패키지 밑에 Entity를 몰아 놓았다. domain이라는 패키지는 enum type의 클래스를 모아두기 위한 것으로 코드성 데이터라고 생각하면 된다. 

security
Spring Security 관련 클래스가 여기 위치 한다.

service
dao가 최소 실행 단위라고 하면, service는 의미 있는 최소 수행 단위라고 해야할 것이다. service에서 제공하는 Method는 dao에서 제공하는 Method를 묶어 의미 있는 서비스를 만들며, 이 서비스는 하나의 트랜잭션으로 묶어 일관성 있는 서비스를 제공해야 한다.

utils : 각종 유틸리티 클래스가 위치할 것이다.




1. ComUserMngt 만들기

6,7번 라인의 @Getter, @Setter는 Lombok 어노테이션을 선언한 것이다. Lombok은 getter/setter를 직접 코딩하지 않아도 생성해주는 도구로 코드를 깔끔하게 유지시켜 준다.


8번 라인의 @ToString 또한 Lombok에서 지원하는 어노테이션으로 멤버 변수 모두를 toString() 메소드에 참여 시켜주는 편리한 어노테이션이다. 그러나 어떤 멤버 변수는 toString() 메소드에 참여시키면 안되는 것도 있다. 특히 JPA의 Entity 클래스와 같은 경우 왜래키 관계를 멤버변수로 표현하는데, toString()으로 풀릴 경우 무한 재귀되는 현상이 있어, 해당 변수는 exclude 항목에 넣어 toString() 메소드에서 제외했다.


9번 라인의 @Entity 어노테이션은 JPA Entity로 참여한 다는 것을 알린다. 테이블에 행당한다고 생각하면 된다.


10번 라인의 @Table은 DB에 테이블로 맵핑되며, 테이블명은 COM_USER_MNGT라는 것을 명시적으론 선언했다.


11번 라인의 @UniqueConstraint은 유니크 인덱스이며, 인덱스 이름은 UK_COM_USER_MNGT_LOGIN_ID이고, 인텍스 컬럼은 LOGIN_ID라는 것을 알린다.


12번 라인의 @SequenceGenerator는 Sequence를 사용한다는 구문으로 17번 라인과 연결된다.


16번 라인의 @ID는 해당 멤버변수가 PK로 참여한다는 것을 알리는 것이며, 17번 라인에서는 해당 PK가 인조키이며, 인조키 사용 방식은 시퀀스 Object를 이용한다는 뜻이다. 이 인조키는 12번에 선언된 SEQGEN_COM_USER_MNGT를 사용한다. JPA에서 인조키는 만드는 방법은 시퀀스를 사용하는 방법 외에도 3가지가 더 있다. 


 인조키 생성 방법

 설명

 적용 DB

 IDENTITY

DB에 의존적이며, 기본키 자동 증가 생성 기능이 있는 DB에서 사용

 MS SQL, MySQL, DB2, PostgresSQL

 SEQUENCE

DB에 의존적이며, DB의 Sequence Object를 사용

 Oracle, H2, PostgreSQL

 TABLE

채번 테이블을 사용해서 사용

 모든 DB

 AUTO

 IDENTITY, SEQUENCE, TABLE 방법 중 하나를 자동 선택

 모든 DB


18번 라인의 @Column 어노테이션은 테이블의 컬럼을 뜻하는 것으로 name속성을 이용해 컬럼명을 명시적으로 지정했다. length속성은 문자 타입의 경우 길이를 지정하는 것으로 LOGIN_ID속성의 길이 제한은 30자이며, nullable=false을 통해 null값이 들어갈 수 없고, unique=true로 지정하므로써 유니크 인덱스를 사용한다는 것을 표시했다. unique 속성은 11번 라인의 @UniqueConstraint 어노테이션과 함께 사용할 수는 없다. 즉, unique=true로 설정하면 11번 라인이 무시되므로 여기서는 삭제하는 것이 마땅하지만, 예제로 넣었다.


36번 라인의 @Column은 아무런 속성이 없는 데 이것에 해당하는멤버 변수가 Date 타입이기 때문에 사용할 수 있는 옵션이 없는 것이다. 대신 37번 라인의 @Temporal(TemporalType.TIMESTAMP)와 같이 처리 함으로써 Date을 값을 저장할 수 있도록 처리 했다.


44번 라인의 @Enumerated(EnumType.STRING)는 enum 타입 클래스를 사용한다는 것이다. @Enumerated는 두가지의 속성을 가지고 있는데, enum 타입의 문자열 값을 사용하던지 멤버 변수의 순번를 사용하던지 할 수 있으며, EnumType.STRING 속성의 순번이 아닌, 문자를 사용한다는 뜻이다. @Enumerated에 들어가 class는 entity.domain 패키지에서 위치시켰으며, 이것은 마치 코드성 데이터와 같다. enum 타입 클래스의 멤버 변수에 해당하는 값만을 넣을 수 있도록 만든다.


54/57번 라인의 @Column의 속성을 보면 updatable=false 속성이 있는데, 이것은 데이터의 생성 시에만 값을 사용하고 수정될 경우는 사용하지 않는 다는 뜻이다. 즉, insert 될 경우만 값이 유효하며, update 문을 사용할 때는 무시된다는 뜻이다. 같은 경우로 61/64번 라인의 경우는 insertable=false 속성을 달고 있으며, 이것은 insert될 때는 무시되고, update될 경우만 값이 입력 된다는 뜻이다.


58번 라인의 @CreationTimestamp의 경우는 입력될 때 creDt 멤버 변수에 값을 넣지 않아도 시간 값을 자동으로 넣어 주는 기능이다. 65번 라인의 @UpdateTimestamp도 같은 역할을 한다. 단, 이것은 생성될 때가 아닌 수정될 때 시간 값을 넣어 준다.





2. ComAuthGrpMngt 만들기

44번 라인의 @OneToMany는 ComAuthGrpMngt객체를 왜래키(Foreign Key) 관계로 바라보는 연관객체가 있다는 뜻이며, 해당 객체는 ComUserAuthGrpMap이라는 것을 알린다. mappedBy="comAuthGrpMngt" 속성의 의미는 ComUserAuthGrpMap에서는 comAuthGrpMngt 멤버변수와 연결되어 있다는 뜻이다.

fetch=FetchType.LAZY속성은 ComAuthGrpMngt에서 특정 값을 조회할 경우 FK관계에 있는 comUserAuthGrpMaps 객체를 자동으로 가져오지 않다는 것을 뜻 한다. 이 값을 FetchType.EAGER 값으로 변경하면, ComAuthGrpMngt를 조회 시 comUserAuthGrpMaps 값을 자동으로 조회되어서 편리할 수도 있다. 하지만 원하지 않는 값을 조회하는 효과를 가질 수 있기 때문에 주의해야 한다. @OneToMany에서는 기본 값이 FetchType.LAZY이지만, 본인은 명시적인 것이 좋아 선언했다.

@OneToMany의 속성 중 cascade = CascadeType.ALL는 연관객체간의 동작을 설명하는 것이다.


 CascadeType 종류

 설명

 CascadeType.RESIST

엔티티를 생성하고, 연관 엔티티를 추가하였을 때 persist() 를 수행하면 연관 엔티티도 함께 persist()가 수행된다.  만약 연관 엔티티가 DB에 등록된 키값을 가지고 있다면 detached entity passed to persist Exception이 발생한다.

 CascadeType.MERGE

트랜잭션이 종료되고 detach 상태에서 연관 엔티티를 추가하거나 변경된 이후에 부모 엔티티가 merge()를 수행하게 되면 변경사항이 적용된다.(연관 엔티티의 추가 및 수정 모두 반영됨)

 CascadeType.REMOVE

삭제 시 연관된 엔티티도 같이 삭제됨

 CascadeType.DETACH

부모 엔티티가 detach()를 수행하게 되면, 연관된 엔티티도 detach() 상태가 되어 변경사항이 반영되지 않는다.

 CascadeType.ALL

모든 Cascade 적용


45번 라인의 @JsonIgnore는 
ComAuthGrpMngt 객체가 json으로 전송될 때 해당 항목은 json으로 변환하지 않도록 한다. 이것이 json으로 풀릴 경우 @ToString과 같이 왜래키 관계에 있는 하위 속성들이 풀리면서 원하지 않는 데이터를 json으로 풀어버릴 수도 있기 때문이다. 




3. ComUserAuthGrpMap 만들기

ComUserAuthGrpMap 클래스는 다른 클래스에 비해 깔끔하다. ComUserAuthGrpMap은 다대다 관계를 유연하게 풀어주는 역할을 하므로, 관계에 의해서만 존재한다. 따라서 ID는 ComUserMngt와 ComAuthGrpMngt를 받는다. 


9번 라인의 @IdClass@ID가 하나가 아니기 때문에 ID를 구성하는 객체를 만들어야 제공해야 한다. 여기서는 ComUserAuthGrpMapId.class라는 클래스를 만들어 제공하며, ComUserAuthGrpMapId.class는 조금 뒤에 설명한다.


13/18번 라인의 @ManyToOne는 @OneToMany와 비슷하지만 다른 의미를 가진다. ComUserAuthGrpMap의 부모에 해당하는 ComUserMngt와 ComAuthGrpMngt에 자식 객체로 묶여 있다는 것을 뜻한다. fetch=FetchType.LAZY 속성은 @OneToMany와 마찬가지로 부모에 해당하는 객체를 자동으로 가져 오지 않는 다는 것을 뜻한다. @ManyToOne의 fetch 타입은 FetchType.EAGER가 기본이 값이다. 그러나 자동 조회하지 못하도록 한 이유는 성능상의 문제가 될 수도 있으며, 원할 경우 자동으로 가져 오는 방법이 있기 때문다.


15/21번 라인의 @JoinColumn은 이것이 일반적인 컬럼이 아니라 왜래키(Foreign Key) 관계에 의해 만들어 진다는 것을 의미한다. 컬럼 이름은 name 속성을 사용하여 부모와 다른 독자적인 컬럼명을 사용할 수 있으나, 속성 타입은 부모의 것을 따르게 된다. 실제 멤버 변수도 부모에 해당하는 ComUserMngt와 ComAuthGrpMngt 객체를 그대로 사용하였다.



9번 라인의 @DataLombok에서 지원하는 어노테이션으로 getter/setter와 hashCode(), equals() 메소드를 자동으로 만들어 준다. @IdClass의 기본 조건 중 상당 부분을 어노테이션 하나로 충족 시켜 준다.

10/11번 라인은 기본 생성자(@NoArgsConstructor)와 모든 멤버 변수를 넣어야 하는 생성자(@AllArgsConstructor)를 만들어 준다.

12번 라이에서 주목해야 하는 부분은 implements Serializable로 @IdClass의 조건중 하나이다.


@IdClass의 조건을 보면 복합키를 가지는 Entity의 경우 빠른 비교와 검색을 위해 hashCode(), equals()를 구현해야 하는 것을 알 수 있다. 단일키 Entity의 경우는 키가 하나이기 때문에 이럴 필요가 없는 것이다.




4. ComUserPwd 만들기

ComUserPwd Entity의 경우 소스 코드 상으로는 특별히 이야기할 만한 것은 없다. 이미 이전 Entity에서 보았던 구성 요소만 있고 새로운 것은 없다. 하지만 ComUserPwd Entity의 경우는 ERD 모델을 생각해 봐야 한다. 


우선, ComUserPwd는 Primary Key는 부모의 키를 상속받지 않는 독자적인 구조이다. 이것을 모델링 이론으로 보면 이상한 구조일 수 밖에 없다. 하위의 자식이 없고, 부모에 종속되는 Entity는 식별관계를 가져야 하기 때문이다. 즉, 부모키와 자신의 키를 합쳐서 복합키로 사용해야 하나 ComUserPwd는 부모의 키는 일반속성으로 낮추고, 자기의 독자적인 키만으로 구성하는 비식별관계를 만든 것이다.


이런 비식별 관계를 만든 이유는 단순히 편리성 때문이다. 복합키를 사용하는 ComUserAuthGrpMap과 같은 구조는 IdClass를 만들어 사용해야 하는 불편함이 존재하기 때문에 아무 이득도 없는 식별관계를 만들어 불편함을 가중할 이유가 없다.




5. application.yml 설정하기

1~16번 라인까지의 속성은 일반적인 설정에 관한 것들이다. 웹서버 포트는 8080을 사용하고 세션은 30분 동안 유지 시키며, 어플리케이션 이름은 demo로 설정하였다. 관심을 가지고 봐야할 부분은 9~14번 라인의 로깅 부분이며, 현재 개발하고 있는 상태에 따라 적절히 추가 변경하면 될 것이다.


18~26번 라인은 데이터 소스에 관한 것이다. 여기서는 단순 테스트용이기 때문 h2 데이터베이스를 사용하고 있으며, 그에 대한 설정이다. h2 데이터베이스에 접속하고자 한다면 h2 데이터베이스 프로그램을 설치하고, H2 Console로 접속해 보면 된다.


28~43번 라인은 JPA 설정에 관한 것이다. 32번 라인의 show-sql은 실행 시 쿼리를 보이라는 것이며, generate-ddl은 DDL문장을 생성하는 기능이다. ddl-auto는 아무것도 하지 않는 none, 스키마를 생성하는 기능이 있는 create와 create-drop, 추가된 속성이 있으면 DDL문장을 갱신해 주는 update, 현재 스키마와 Entity 클래스의 정의가 동일한지 확인하는 validate 등이 있다.




6. 실행하기

이 프로그램을 실행시키기 위해서는 우선 H2 Database를 실행시켜줘야 한다. H2의 실행은 H2 Console를 실행 시킴으로써 가능한다. 실행시 접속 JDBC URL은 application.yml에 입력한 것과 동일한 것을 사용하면 된다. 

예) jdbc:h2:tcp://localhost/~/demodb





H2 Console를 실행 시켰으면, STS에서 Boot Dashboard를 찾아 demo 프로그램을 실행 시키면 된다.

실행은 (Re)start 또는 (Re)debug 중 아무거나 실행시키자. 





STS의 Console의 로그를 보면 아래와 같이 DDL문장이 만들어지고 실행되었음을 보인다.




마지막으로 H2 Console에 접속하면 해당 테이블이 만들어 졌음을 확인할 수 있다.





아래는 지금까지의 소스를 압축한 파일이다.

정말 잘되는지 궁금하면 실행해 보는 것도 좋을 것이다.


demo.zip