728x90

쿠키는 서버에서 클라이언트에 데이터를 저장하는 방법으로

서버에서 클라이언트에 쿠키를 저장하는 것만 의미하는 것이 아니라

클라이언트에서 서버로 쿠키를 전송하는 것도 포함된다.

 

쿠키의 특징

 

서버가 클라이언트에 데이터를 저장할 수 있지만 가져올 때는 조건이 있다

쿠키 옵션을 만족하는 경우에만 클라이언트로부터 데이터를 가져올 수 있다.

 

다양한 쿠키 옵션이 존재하지만 주로 사용되는 것들은

Domain, Path, MaxAge, Expires, Secure, HttpOnly, SameSite 등이 있다.

 

Domain

https://n.news.naver.com/mnews/article/003/0011960011?sid=104

도메인은 서버에 접속할 수 있는 이름을 말하며

위의 주소에서는 naver.com 부분이 도메인에 해당하고

n.news 부분을 서브 도메인이라고 한다.

 

쿠키 옵션에 도메인 정보가 존재한다면 클라이언트는

쿠키와 서버 양쪽의 도메인이 일치해야만 쿠키를 전송할 수 있다.

 

Path

서버가 라우팅 할 때 사용하는 경로로

mnews/article 이런 부분들에 해당한다.

 

만약 Path가 mnews로 설정되어 있다면

해당 패스 뒤에 오는 추가 패스들에도 쿠키 전송이 가능하다

즉, mnews/article 은 가능하지만 xnews/article 같은 경우는 패스가 다르기 때문에 안된다.

 

MaxAge / Expires

쿠키가 언제까지 유효하게 할지 기간을 정하는 옵션이다.

 

MaxAge는 앞으로 몇 초 동안 쿠키가 유효한지 설정한다면

Expires는 언제까지 유효한지 Date를 지정한다.

 

해당 옵션들이 없는 쿠키는 브라우저가 실행 중일 때마 사용할 수 있고

브라우저 종료 시 삭제되기 때문에 세션 쿠키라고 한다.

 

해당 옵션들이 적용되면 지정된 기간만큼 사용가능한 쿠키이기 때문에

영속성 쿠키라고 한다.

 

Secure

쿠키를 전송해야 할 때 사용하는 프로토콜에 따른 전송 여부를 결정할 때 사용한다.

 

해당 옵션이 true라면 HTTPS 프로토콜을 사용한 통신만 가능하며

false라면 모든 프로토콜의 통신이 가능하다.

 

HttpOnly

자바 스크립트에서 브라우저의 쿠키에 접근을 허용할지 결정할 때 사용한다.

 

자바 스크립트에서 쿠키에 접근을 하는 것은 XSS 공격에 취약하기 때문에

기본값인 false 대신 true로 설정하여 접근하지 못하게 하는 것이 좋다.

 

SameSite

Cross-Site 요청을 받은 경우에 요청에서 사용한 메서드와 해당 옵션의 조합을 기준으로

서버의 쿠키 전송 여부를 결정한다.

Cross-Origin : 도메인, 프로토콜, 포트 중 하나라도 다른 경우
Cross-Site : eTLD+1이 다른 경우로, TLD는 .com, .org 같은 최상위 도메인을 말하며,
최상위 도메인의 왼쪽 하위 레벨의 도메인을 합친 것을 eTLD+1라고 한다.

https://da9dac.github.io
https://da8dac.github.io
io 같은 경우는 github.io를 하나의 TLD로 보기 때문에
eTLD+1에 해당하는 부분은 da9dac과 da8dac다.

위의 주소들은 eTLD+1가 서로 다르기 때문에 Cross-Site에 해당한다.

SameSite의 옵션은 아래와 같다.

  • Lax : Cross-Site 요청이라면 GET 메서드에 대해서만 쿠키를 전송
  • Strict : SameSite 요청인 경우에만 쿠키를 전송
  • None : 항상 쿠키를 전송하지만 보안에 취약하여 Secure 옵션 필요

 

쿠키를 이용한 상태 유지

HTTP는 무상태성을 가지지만 이러한 쿠키의 특성을 이용하여상태를 가지고 통신을 할 수 있게 만들 수 있다.

 

하지만 쿠키는 오랜 시간 유지될 수 있고, 자바 스크립트를 통해 접근할 수 있어보안에 취약한 점이 있기 때문에 민감한 정보를 담는 것은 좋지 않다.

'Back-End > Security' 카테고리의 다른 글

[스프링 시큐리티] 회원가입 구현  (0) 2023.07.10
[스프링 시큐리티] 기본  (0) 2023.07.10
Session  (0) 2023.07.07
Hashing  (0) 2023.07.07
HTTPS  (0) 2023.07.07
728x90

복호화가 불가능하고 암호화만 가능한 암호화 방식

 

해시 함수를 사용하여 암호화를 진행하며 아래와 같은 특성이 있다.

  • 항상 같은 길이의 문자열을 리턴한다
  • 문자열에 해시 함수 적용 시 서로 다른 문자열은 항상 다른 결과를 반환한다.
  • 반대로 서로 같은 문자열은 항상 같은 결과를 반환한다.
  • 같은 문자라면 결과가 항상 같다는 것

레인보우 테이블

항상 같은 결과를 반환하는 특성을 이용해 해시 함수 적용 전의 값을

알아낼 수 있도록 기록해두는 테이블

 

이 테이블이 유출되는 경우 해싱이 되었더라도 이전의 값을 알아낼 수 있어 위험하다.

 

솔트

단어 그대로 소금을 치듯 해싱 이전 값에 임의의 값을 더하여

데이터가 유출되어도 데이터를 알아내기 어렵게 만드는 방법

 

해싱 값이 유출되더라도 솔트가 유출되는 것이 아니라면 안전함

 

복호화가 불가능한 이유

해싱은 데이터를 사용하는 것이 목적이 아닌

데이터가 동일한지만 확인하는 것이 목적이기 때문에

같은 해싱값이라면 동일한 데이터이니 굳이 복호화를 할 필요가 없다.

 

그래서 비밀번호와 같은 데이터가 동일한지만 판단하면 되고

데이터 유출의 위험성이 없어야하는 부분에 대해서는

주로 해싱 기법을 사용한다.

'Back-End > Security' 카테고리의 다른 글

[스프링 시큐리티] 회원가입 구현  (0) 2023.07.10
[스프링 시큐리티] 기본  (0) 2023.07.10
Session  (0) 2023.07.07
Cookie  (0) 2023.07.07
HTTPS  (0) 2023.07.07
728x90

Hyper Text Transfer Protocol + Secure Socket layer의 약자로

기존의 HTTP에 보안 기능을 추가한 계층이라고 볼 수 있다.

 

HTTP 요청을 SSL 혹은 TLS라는 알고리즘을 이용해

통신 과정에서 데이터를 암호화하여 전송하는 방법이다.

 

HTTPS의 장점

암호화

서버와 클라이언트가 주고 받는 정보를 중간에 제 3자가 탈취할 수 없도록

정해둔 방법으로 데이터를 암호화하여 주고 받기 때문에

데이터가 탈취되어도 데이터를 알아볼 수 없다.

 

클라이언트와 서버 간의 데이터는 비대칭키와 대칭키 방식을 혼용해서 사용하기 때문에

데이터를 복호화하기 전까지 제 3자는 데이터를 알 수가 없기 때문이다.

 

클라이언트와 서버 간의 데이터를 주고 받을 때는 양쪽이 비밀 키를 공유하여

데이터를 암호화하고 복호화 하는 대칭키 방식을 사용하고

대칭키를 주고 받을 때는 비대칭키를 사용한다.

 

대칭키는 비밀 키를 사용하여 복호화가 쉽기 때문에 클라이언트와 서버 간에 사용하지만

대칭키가 탈취되는 경우에는 복호화 될 가능성이 높기 때문에

대칭기를 주고 받는 과정에서는 개인이 가진 비밀 키로만 풀 수 있는

비대칭키 방식으로 주고 받아 복호화 할 수 없게 한다.

 

  • 대칭키 : 하나의 비밀 키를 서로 공유하여 암호화와 복호화에 사용함
  • 비대칭키 : 공개 키를 사용하여 암호화를 하지만 복호화는 개인 키로만 가능함

 

인증서

브라우저가 서버의 응답과 같이 전달된 인증서를 확인할 수 있기 때문에

서버의 신원을 보증해준다.

 

이러한 서버의 신원을 보증해주는 제 3자를 CA(Certificate Authority)라고 부르며

인증서를 발급해 주는 공인 기관들로 서버의 공개키와 정보를 비밀키로 암호화하여 인증서를 발급한다.

 

인증서를 통해 검증하는 과정은 아래와 같다.

  1. 서버가 클라이언트에 인증서를 전달
  2. 클라이언트 측의 운영체제나 브라우저에 내장된 CA 리스트에서 조회
  3. 인증된 CA에서 발급 받은 인증서인지 확인
  4. 아니라면 경고창을 띄워서 연결이 안전하지 않다 알림
  5. 인증된 인증서라면 해당 CA 기관의 공개키로 인증서를 복호화
  6. 복호화해 얻은 공개키로 서버가 신뢰할 수 있는 대상인지 판단

'Back-End > Security' 카테고리의 다른 글

[스프링 시큐리티] 회원가입 구현  (0) 2023.07.10
[스프링 시큐리티] 기본  (0) 2023.07.10
Session  (0) 2023.07.07
Cookie  (0) 2023.07.07
Hashing  (0) 2023.07.07
728x90

슬라이스 테스트

슬라이스라는 단어처럼 자기가 테스트하고자 하는 계층 외에

나머지 계층들과는 단절하여 오로지 하나의 계층만 테스트하는 방법이다.

 

컨트롤러 슬라이스 테스트

서비스 영역 같은 다른 영역을 사용하지 않고 컨트롤러만 사용하여

테스트를 하고자 하는 경우에는 기본적인 양식은 아래와 같다.

@WebMvcTest(AnswerController.class)
public class QuestionControllerTest {
    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private Gson gson;

    @MockBean
    private QuestionService questionService;

    @MockBean
    private QuestionMapper questionMapper;
    
    // 테스트 코드들
}

@WebMvcTest(테스트하려는 컨트롤러의 클래스)

해당 어노테이션을 사용하면 컨트롤러 테스트에 필요한 빈만 사용하여 테스트를 수행한다.

@SpringBootTest 어노테이션은 등록된 모든 빈을 사용하여 테스트를 수행하기 때문에

속도적인 측면에서 슬라이스 테스트에 사용하긴 효율적이지 못하다.

 

MockMvc

컨트롤러를 테스트하기 위해 API를 호출하고 검증하는 기능을 제공한다.

 

Gson

객체를 json 형식으로 바꿀 때 사용한다. (json 형식 이외에도 다른 형식으로도 바꿀 수 있다.)

 

@MockBean

테스트에 사용할 목 객체(가짜 객체)를 빈으로 등록하여 실제 빈 대신에 동작하게 한다.

스프링 테스트 프레임워크와 함께 쓰이며 통합 테스트에서 스프링 빈을 목 객체로 대체하려 사용한다.

 

@MockBean과 @Mock

두 어노테이션 모두 테스트 과정에 사용 할 목객체를 생성하고 대체하는 것은 똑같지만

어떤 프레임워크와 같이 사용되고 어느 테스트에 사용되는지가 다르다.

 

@MockBean 어노테이션은 위에서 언급했듯이 스프링 테스트 프레임워크와 함께 사용하여

주로 통합 테스트에 사용된다.

 

@Mock 어노테이션은 Mockito 프레임워크와 함께 사용되어 주로 단위 테스트에 사용된다.

 

컨트롤러 테스트에서는 스프링에서 제공하는 WebMvcTest를 사용하기 때문에

@MockBean 어노테이션을 사용한다.

 

@Test
public void postQuestionTest() throws Exception {
    long memberId = 1L;
    Member member = new Member();
    member.setMemberId(memberId);

    Question question = new Question();
    question.setQuestionTitle("Question Title");
    question.setQuestionText("Question Text");
    question.setQuestionStatus(Question.QuestionStatus.QUESTION_REGISTERED);
    question.setQuestionOpen(Question.QuestionOpen.SECRET);
    question.setMember(member);

    QuestionDto.Post postDto = new QuestionDto.Post(
            "Question Title",
            "Question Text",
            Question.QuestionOpen.PUBLIC
    );

    String requestContent = gson.toJson(postDto);

    given(questionMapper.questionPostToQuestion(Mockito.any(QuestionDto.Post.class)))
    .willReturn(new Question());
    
    given(questionService.createQuestion(Mockito.any(Question.class), Mockito.anyLong()))
    .willReturn(question);

    mockMvc.perform(
            post(root + "/create")
                    .param("memberId", String.valueOf(memberId))
                    .contentType(MediaType.APPLICATION_JSON)
                    .content(requestContent)
    )
            .andExpect(status().isCreated())
            .andExpect(jsonPath("$.questionTitle").value(question.getQuestionTitle()));
}

POST 컨트롤러의 간단한 테스트 코드를 나눠서 살펴보겠다.

long memberId = 1L;
Member member = new Member();
member.setMemberId(memberId);

Question question = new Question();
question.setQuestionTitle("Question Title");
question.setQuestionText("Question Text");
question.setQuestionStatus(Question.QuestionStatus.QUESTION_REGISTERED);
question.setQuestionOpen(Question.QuestionOpen.SECRET);
question.setMember(member);

given에 사용될 데이터를 미리 하나 만들어둔다.

given 부분을 설명할 때 다시 설명하겠다.

QuestionDto.Post postDto = new QuestionDto.Post(
                "Question Title",
                "Question Text",
                Question.QuestionOpen.PUBLIC
        );

String requestContent = gson.toJson(postDto);

실제 클라이언트가 요청에 사용할 DTO 객체를 하나 만든 후에

해당 객체를 Gson을 사용해 json 형태로 변환한다.

given(questionMapper.questionPostToQuestion(Mockito.any(QuestionDto.Post.class)))
.willReturn(new Question());

QuestionMapper와 QuestionService 객체들은 컨트롤러만 슬라이스 테스트 하기 위해서는

불필요한 객체들이기 때문에 어떤 값을 전달 받아서 실행이 되어도

항상 정해진 결과를 반환하게 고정한다.

 

문법이 어렵게 느껴질 수도 있지만 코드를 읽어 보면 어렵지 않다.

 

QuestionMapper 객체의 questionPostToQuestion 메서드에

어떤 QuestionDto.Post 타입의 객체가 전달되어도

항상 Question 객체를 반환하겠다는 것이다.

given(questionService.createQuestion(Mockito.any(Question.class), Mockito.anyLong()))
.willReturn(question);

첫 given에서는 직접 정의해둔 결과가 아닌 임의의 새로운 질문 객체를 리턴하도록 지정했지만

마지막 given에서는 직접 정의해둔 객체를 리턴하도록 지정했다.

 

해당 컨트롤러에서는 QuestionService 객체의 createQuestion 메서드를 호출한

결과값이 리턴값이기 때문에 결과를 직접 정의한 객체로 정해줬다.

 

컨트롤러의 테스트는 결국에 클라이언트의 요청과 서버의 응답만 테스트하는 것이

목적이기 때문에 응답에 영향을 미치지 않는 중간 과정들은 어떤 값을 리턴하든 상관 없고

최종적인 결과에 영향을 미치는 과정만 리턴 값을 정의해주면 된다.

mockMvc.perform(
        post(root + "/create")
                .param("memberId", String.valueOf(memberId))
                .contentType(MediaType.APPLICATION_JSON)
                .content(requestContent)
)
        .andExpect(status().isCreated())
        .andExpect(jsonPath("$.questionTitle").value(question.getQuestionTitle()));

지금까지는 테스트를 수행하기 위한 준비 과정이었다면

이제 MockMvc의 perform 메서드를 사용해 컨트롤러 테스트를 할 수 있다.

post(root + "/create")

위의 코드처럼 어떤 종류의 요청을 테스트할지 정한 후에

해당 요청을 받을 주소를 설정해준다.

.param("memberId", String.valueOf(memberId))
.contentType(MediaType.APPLICATION_JSON)
.content(requestContent)

해당 요청에 대한 추가적인 정보들을 지정해주면 요청에 대한 응답 데이터를 담은

ResultActions 객체가 반환된다.

 

위의 코드에서는 주소의 파라미터 값(?memberId=1)을 param 메서드를 사용해 지정해주고

기존에 Gson을 사용해 json 형식으로 만들어두었던 요청 데이터를

클라이언트가 요청을 보낼 때 사용할 데이터로 지정해준다.

.andExpect(status().isCreated())
.andExpect(jsonPath("$.questionTitle").value(question.getQuestionTitle()));

ResultActions 객체가 가지고 있는 데이터를 사용해

 

서비스 슬라이스 테스트

컨트롤러 슬라이스와 마찬가지로 서비스도 서비스가 의존하고 있는 객체과의

관계를 끊고 목 객체를 이용하여 테스트하면 된다.

@Service
public class MemberService {
    private final MemberRepository memberRepository;

    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
}

위와 같은 구조의 서비스 클래스를 테스트 해보자

@ExtendWith(MockitoExtension.class)
public class MemberServiceTest {
    @Mock
    private MemberRepository memberRepository;

    @InjectMocks
    private MemberService memberService;
    
    // 테스트 코드 생략
}

서비스 슬라이스 테스트는 기본적으로 위와 같은 양식을 가진다.

 

@ExtendWith(추가할 확장 클래스)

JUnit5의 확장 모델을 사용하기 위해 사용하는 어노테이션이다.

현재 테스트 클래스에서는 Mockito 확장을 추가하기 위해 사용하였다.

 

@Mock

컨트롤러 테스트에서 알아봤듯이 Mockito 프레임워크를 사용할 때

목 객체를 생성하기 위한 어노테이션이다.

 

@InjectMocks

@Mock 어노테이션이 적용된 객체들을 해당 어노테이션이 적용된 객체에 주입한다.

 

서비스 클래스에서 의존관계를 주입하는 것과 같은 작업이라고 볼 수 있다.

@Test
public void saveMemberTest() throws Exception{
    Member member = new Member();
    member.setMemberName("이름");
    member.setMemberId(1L);
    member.setMemberEmail("test@email.com");

    given(memberRepository.save(Mockito.any(Member.class))).willReturn(member);

    assertThat(
            memberService.createMember(member).getMemberName(),
            is(equalTo(member.getMemberName()))
    );
}

테스트 하는 방식은 컨트롤러와 다를게 없다.

'Back-End > Spring' 카테고리의 다른 글

[로깅] MDC를 이용한 식별된 로깅 처리  (1) 2024.03.05
[JPA] N + 1 문제와 해결 방법  (1) 2024.03.01
예외  (0) 2023.06.15
템플릿 정리  (0) 2023.06.14
JDBC Template  (0) 2023.06.13
728x90

ManyToOne

다대일 관계의 반대 방향은 항상 일대다 관계이며 일대다 관계의 반대 방향도 다대일 관계다.

 

데이터베이스에서 일대다 관계에서 외래 키는 항상 다쪽이 가지고 있기 때문에

객체의 양방향 관계에서의 주인도 항상 다쪽이다.

 

양방향 연관관계는 항상 서로를 참조해야 한다.

 

setTeam, addMember 같은 편의 메서드를 작성하는 것이 좋지만

양쪽에 모두 작성하는 경우에는 무한루프에 빠질 수 있으니 검사해줘야 한다.

 

OneToMany

일대다 관계는 엔티티를 하나 이상 참조할 수 있어서 컬렉션 타입을 사용한다.

 

일대다 단방향 관계를 매핑할 때 @JoinColumn 어노테이션을 명시하지 않으면

연결 테이블을 중간에 두고 연관관계를 관리하는 조인 테이블 전략을 사용하여

반드시 명시해주는게 좋다.

 

일대다 단방향 매핑은 결국 외래키를 직접 가지고 있는 것이 아니기 때문에

연관관계 처리를 위한 UPDATE SQL을 추가로 실행해야 한다.

 

그래서 일대다 단방향 매핑은 반대 테이블의 외래 키를 관리해야 하기 때문에

성능과 관리적인 부분에서 좋지 않다.

 

일대다 단방향 매핑보다는 관리해야 하는 외래 키가 본인 테이블에 있는

다대일 양방향 매핑을 사용하는 것이 좋다.

 

OneToMany : ManyToOne (일대다 양방향)

다대일 양방향 매핑에서 연관관계의 주인은 항상 다쪽인 ManyToOne이기에

@ManyToOne 속성에는 mappedBy가 없는 것이다.

 

일대다 양방향 매핑을 사용할 일이 없지만 아래와 같이 매핑 할 수는 있다.

public class Team {
	// 기존 코드 생략
    @OneToMany
    @JoinColumn(name = "TEAM_ID")
    private List<Member> members = new ArrayList<Member>(); 
}

public class Member {
	// 기존 코드 생략
    @ManyToOne
    @JoinColumn(name = "TEAM_ID", insertable = false, updatable = false)
    private Team team;
}

위의 코드처럼 양쪽 모두에 같은 외래키를 사용하게 매핑을 하는데

이때 다쪽의 외래키를 읽기 전용으로 만들어 양쪽에서 같은 외래키를 관리하지 못하게 한다.

 

일대다 양방향이라기 보단 반대쪽은 읽기 전용으로 만들기 때문에

일대다 단방향 매핑이 가지는 단점을 그대로 가셔서 다대일 양방향 매핑을 사용하는 것이 좋다.

 

OneToOne

한 명의 회원이 하나의 장바구니만 가지는 것과 같은 관계로

아래와 같은 특징을 가지고 있다.

  • 일대일 관계에서는 반대 쪽도 항상 일대일 관계다.
  • 둘 중 어느 곳이나 외래 키를 가질 수 있다.

일대일 관계는 둘 다 외래키를 가질 수 있기 때문에 누가 외래키를 가질지 지정해야한다.

주 테이블에 외래 키를 두는 방법

주 객체가 대상 테이블을 참조하는 것처럼 주 테이블(회원)이 장바구니를 참조한다.

 

외래키를 객체 참조와 비슷하게 사용할 수 있어서 객체지향 개발자들이 선호하며

주 테이블만 확인해도 연관관계를 확인 할 수 있다.

@Entity
public class Member {
	// 기본 필드 생략
    @OneToOne
    @JoinColumn(name = "LOCKER_ID")
    private Locker locker;
}

@Entity
public class Locker {
    @Id
    @GeneratedValue
    @Column(name = "LOCKER_ID")
    private Long id;
}

위의 코드처럼 단방향으로 매핑하거나

@Entity
public class Locker {
    // 기존코드 생략
    @OneToOne(mappedBy = "locker")
    private Member member;
}

위의 코드처럼 양방향으로 매핑할 수 있다.

양방향 매핑이니 당연히 연관관계의 주인을 설정해줘야하는데

주 테이블인 멤버 엔티티의 외래키 컬럼을 주인으로 설정한다.

대상 테이블에 외래 키를 두는 방법

JPA에서는 일대일 관계에서 대상 테이블에 외래 키가 있는 단방향 관계를 지원하지 않기 때문에

대상 테이블에서 주 테이블로 단방향 관계를 수정하거나

양방향 관계로 만들고 대상 테이블을 연관관계의 주인으로 설정해야 한다.

@Entity
public class Member {
	// 기본 필드 생략
    @OneToOne(mappedBy = "member")
    private Locker locker;
}

@Entity
public class Locker {
    // 기본 필드 생략
    @OneToOne
    @JoinColumn(name = "MEMBER_ID")
    private Member member;
}

위와 같이 일대일 매핑에서 대상 테이블에 외래키를 두기 위해선

양방향으로 설정한 후에 연관관계의 주인을 주 테이블이 아닌

대상 테이블로 설정한다.

 

ManyToMany

관계형 데이터베이스에서는 테이블 2개로 다대다 관계를 표현할 수 없기 때문에

다대다 관계를 다대일 관계로 풀어내는 연결 테이블을 사용하지만

객체는 테이블과 다르게 객체 2개로 다대다 관계를 만들 수 있다.

단방향

@Entity
public class Member {
	// 기본 필드 생략
	@ManyToMany
    @JoinTable(name = "MEMBER_PRODUCT", joinColumns = @JoinColumn(name = "MEMBER_ID"),
    			inverseJoinColumns = @JoinColumn(name = "PRODUCT_ID"))
	private List<Product> products = new ArrayList<Product>();
}

위와 같이 @ManyToMany 어노테이션을 사용하여 다대다 단방향 관계를 설정할 수 있으며

@JoinTable 어노테이션을 사용하여 연결 테이블을 바로 매핑할 수 있다.

 

@JoinTable의 속성은 아래와 같다.

  • name : 연결 테이블을 지정
  • joinColumns : 현재 방향인 회원과 매핑할 조인 컬럼 정보를 지정 (회원 테이블의 기본키)
  • inverseJoinColumns : 반대 방향인 상품 테이블과 매핑할 조인 컬럼 정보를 지정 (상품 테이블의 기본키)

양방향

@Entity
public class Product {
	// 기본 필드 생략
	@ManyToMany(mappedBy = "products")
	private List<Member> members;
}

반대쪽 엔티티에도 매핑을 해주면 양방향 관계로 설정할 수 있다.

양방향 연관관계 편의 메서드

member.getProducts().add(product);
product.getMembers().add(member);

위와 같이 연관관계를 설정해주는 반복적인 작업들을

아래 코드와 같이 주 테이블에 메서드로 만들어 편리하게 사용하는 것이 좋다.

@Entity
public class Member {
	// 기존 코드 생략
    public void addProduct(Product product) {
    	products.add(product);
        product.getMembers().add(this);
    }
}

member.addProduct(product);

@ManyToMany 어노테이션을 사용한 매핑의 단점

어노테이션을 사용하여 연결 테이블을 자동으로 매핑하면 편리하지만

관계를 맺는 양쪽 테이블의 기본키만 외래키로 가지고 있기 때문에

추가적인 컬럼이 필요한 경우 추가할 수가 없다.

 

그래서 N:M 관계를 직접 연결 테이블을 추가하여 매핑하는 것이 좋다.

 

만약 회원과 주문이 N:M 관계라면

회원N : M주문 >> 회원1 : N:연결 테이블M : 1주문 형태로 바꿀 수 있다.

@Entity
public class Member {
	@OneToMany(mappedBy = "member")
    private List<MemberProducts> memberProducts;
}

@Entity
@IdClass(MemberProductId.class)
public class MemberProduct {
	@Id
	@ManyToOne
    @JoinColumn(name = "MEMBER_ID")
    private Member member;
    
    @Id
	@ManyToOne
    @JoinColumn(name = "PRODUCT_ID")
    private Product product;
}

@Entity
public class Product {
	@OneToMany(mappedBy = "product")
    private List<MemberProducts> memberProducts;
}

위의 코드는 다대다 양방향 관계를 코드로 표현한 예시다.

 

만약 회원상품 엔티티처럼 복합 키를 사용하는 경우에는

식별자 클래스가 추가로 필요하다.

public class MemberProductId implements Serializable {
	// 회원멤버 엔티티의 member / product와 연결
	private String member;
    private String product;
    
    // hashCode, equals 메서드 오버라이딩 하기
}

위의 코드처럼 복합 키는 별도의 식별자 클래스를 사용해서 만들어야 하며

Serializable을 구현하여야 하며 hashCode, equals 메서드를 구현해야 하고

기본 생성자가 있어야 하며 해당 클래스는 public이어야 한다.

@Entity
public class Order {
	@Id
    @GeneratedValue
    @Column(name = "ORDER_ID")
    private Long id;

	@ManyToOne
    @JoinColumn(name = "MEMBER_ID")
    private Member member;
    
	@ManyToOne
    @JoinColumn(name = "PRODUCT_ID")
    private Product product;
}

복합 키를 사용하는 방법은 번거롭기 때문에 위의 코드처럼

별도의 기본 키를 사용하는 것이 편하다.

더보기

실습

@Entity
public class Member {
	@Id
    @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;

	@Column(name = "MEMBER_NAME")
    private String name;
    
    @Column(name = "MEMBER_CITY")
    private String city;
    
    @Column(name = "MEMBER_STREET")
    private String street;
    
    @Column(name = "MEMBER_ZIPCODE")
    private String zipcode;
    
    @OneToMany(mappedBy = "member")
    private List<Order> orders = new ArrayList<Order>();
}

@Entity
@Table(name = "ORDERS")
public class Order {
	@Id
    @GeneratedValue
    @Column(name = "ORDERS_ID")
    private Long id;
    
    @ManyToOne
    @JoinColumn(name = "MEMBER_ID")
    private Member member;
    
    @OneToOne
    @JoinColumn(name = "DELIVERY_ID")
    private Delivery delivery;
    
    @Column(name = "ORDERDATE")
    private LocalDateTime orderDate = LocalDateTime.now();
    
    @Enumerated
    @Column(name = "ORDERS_STATUS")
    private OrderStatus status = OrderStatus.ACTIVE;
    
    public enum OrderStatus {
    	ACTIVE,
        STOP
    }
}

@Entity
public class Delivery {
	@Id
    @GeneratedValue
    @Column(name = "DELIVERY_ID")
    private Long id;
    
    @Column(name = "DELIVERY_CITY")
    private String city;
    
    @Column(name = "DELIVERY_STREET")
    private String street;
    
    @Column(name = "DELIVERY_ZIPCODE")
    private String zipcode;
    
    @Enumerated(EnumType.STRING)
    @Column(name = "DELIVERY_STATUS")
    private DeliveryStatus status = DeliveryStatus.START;
    
    public enum DeliveryStatus {
    	START,
        END
    }
}

@Entity
public class OrderItem {
	@Id
    @GeneratedValue
    @Column(name = "ORDER_ITEM_ID")
    private Long id;
    
    @ManyToOne
    @JoinColumn(name = "ORDER_ID")
    private Order order;
    
    @ManyToOne
    @JoinColumn(name = "ITEM_ID")
    private Item item;
    
    @Column(name = "ORDERPRICE")
    private int orderPrice;
    
    private int count;
}

@Entity
public class Item {
	@Id
    @GeneratedValue
    @Column(name = "ITEM_ID")
    private Long id;
    
    @Column(name = "ITEM_NMAE")
    private String name;
    
    @Column(name = "ITEMPRICE")
    private int itemPrice;
    
    private int stockQuantity;
}

@Entity
@IdClass(CategoryItemId.class)
@Table(name = "CATEGORY_ITEM")
public class CategoryItem {
	@Id
    @ManyToOne
    @JoinColumn(name = "ITEM_ID")
    private Item item;
    
    @Id
    @ManyToOne
    @JoinColumn(name = "CATEGORY_ID")
    private Category category;
}

public class CategoryItemId implements serializable {
	private String item;
    private String category;
    
    @Override
    // equals 및 hashCode 오버라이딩
}

@Entity
public class Category {
	@Id
    @GeneratedValue
    @Column(name = "CATEGORY_ID")
    private Long id;
}

'Back-End > JPA' 카테고리의 다른 글

연관관계 매핑 기초  (0) 2023.06.20
엔티티 매핑 : 학습을 위한 스키마 자동 생성하기  (0) 2023.06.19
엔티티 매핑 : 매핑 어노테이션  (0) 2023.06.19
영속성과 엔티티  (0) 2023.06.01
JPA 시작하기  (0) 2023.06.01
728x90

단방향 연관관계

 

 

만약 위와 같은 연관관계의 테이블이 있다고 했을 때

멤버는 하나의 팀에만 소속될 수 있지만

팀은 여러 명의 회원을 가질 수 있는 다대일(멤버:팀) 관계이다.

 

테이블과의 연관관계는 한 쪽에서만 외래키를 참조하면

어느 쪽에서든 조인할 수 있는 양방향 관계이다.

@Entity
@Table
@Getter @Setter
public class Member {
	@Id
    @Column(name = "member_id")
	private int id;
    
    @Column(name = "member_name")
    private String memberName;
    
    @ManyToOne
    @JoinColumn(name = "team_id")
    private Team team;
}

@Entity
@Table
@Getter @Setter
public class Team {
	@Id
    @Column(name = "team_id")
	private int Id;
    
    @Column(name = "team_name")
    private String teamName;
}

하지만 객체에서는 참조하고자 하는 필드에서

팀 객체를 필드로 선언하여 연관관계를 맺는다.

 

테이블과는 다르게 객체에서의 연관관계의 방향성은

객체를 참조한 쪽에서만 접근할 수 있는 단방향 관계다.

 

참조를 통한 연관관계는 언제나 단방향이기에 양방향으로 만들려면

반대쪽에서도 객체 참조를 해줘야한다.

 

하지만 이건 엄밀히 따진 서로 다른 단방향 관계 두 개라고 보는게 맞다.

Member member1 = new Member(1,"고길동");
Member member2 = new Member(2,"신짱구");
Team team1 = new Team(1,"팀1");

member1.setTeam(team1); // Member 객체의 Setter를 통해 연관관계 설정
member2.setTeam(team1);

Team findTeam = member1.getTeam(); // Member 객체의 Getter를 통해 객체 참조 탐색

객체는 위와 같이 참조를 사용해 연관관계를 탐색하며

이러한 탐색을 객체 그래프 탐색이라고 한다.

다대일 관계 매핑

@ManyToOne
@JoinColumn(name = "team_id")
private Team team;

참조하고자 하는 객체의 필드에서 참조하려는 객체를 필드에 선언한다.

해당 필드에는 @ManyToOne 어노테이션과 @JoinColumn 어노테이션을 추가한다.

@ManyToOne

다대일 관계를 정의할 때 사용하는 어노테이션으로

하나의 팀(One)에 여러 멤버(Many)가 속할 수 있으니

위의 코드에서는 Many(Member) To One(Team)이라고 볼 수 있다.

 

아래와 같은 속성들을 사용할 수 있다.

  • optional : 기본값은 true로 false로 설정하면 연관된 엔티티가 항상 있어야 함
  • fetch : 글로벌 패치 전략을 설정한다. 기본값은 FetchType.Eager
  • cascade : 영속성 전이 기능을 사용한다
  • targetEntity : 연관된 엔티티의 타입 정보를 설정한다. 거의 사용하지 않음

@JoinColumn

외래키 를 매핑할 때 사용하는 어노테이션

 

아래와 같은 속성들을 사용할 수 있다.

  • name : 매핑할 외래 키 이름을 지정하며 미지정시 필드명_참조하는 기본 키 컬럼명
  • referencedColumnName : 외래 키가 참조하는 대상 테이블의 컬럼명
  • foreignKey(DDL) : 외래 키 제약조건을 직접 지정하며 테이블을 생성할 때만 사용
  • 추가로 @Column 어노테이션의 속성도 사용가능

 

연관관계 사용

저장

Team team1 = new Team(1, "팀1"); // 비영속
em.persist(team1); // 영속

Member member1 = new Member(1, "홍길동"); // 비영속
member1.setTeam(team1); // 비영속 연간관계 설정
em.persist(member1); // 영속

JPA에서 엔티티를 저장하려면 연관된 모든 엔티티까지 영속 상태여야 하기 때문에

팀과 멤버 엔티티를 모두 영속 상태로 만든다.

조회

연간관계가 있는 엔티티의 조회 방법은 두 가지가 있다.

Team findTeam = member1.getTeam();

첫 번째 방법은 이전에 살펴보았던 객체 그래프 탐색이 있다.

필드에 참조하고 있는 객체를 사용하여 조회한다.

String jpql = "select m from Member m join m.team t where t.name = :teamName"

List<Member> resultList = em.createQuery(jpql, Member.class)
	.setParameter("teamName", "팀1");
    .getResultList();

두 번째 방법은 객체지향 쿼리인 JPQL을 사용하는 방법이다.

 

객체 참조 방식으로는 데이터베이스의 조인을 사용할 수 없었지만

이 방식을 사용하면 조인을 사용하여 조회할 수 있다.

String jpql = "select m from Member m join m.team t where t.name = :teamName"

위의 코드를 살펴보면 변수 jpql에 쿼리문을 담아두는데

여기서 사용되는 m과 t는 각각 멤버와 팀 테이블의 별칭을 지정한 것이며

:teamName 같은 부분은 이후에 파라미터로 값을 받아올 부분이다.

 

쿼리문으로 변환하면 아래와 같다.

 

SELECT * FROM MEMBER AS M JOIN TEAM ON M.TEAM_ID = T.TEAM_ID

WHERE TEAM_NAME = "파라미터로 받아올 팀명";

List<Member> resultList = em.createQuery(jpql, Member.class)
	.setParameter("teamName", "팀1");
    .getResultList();

해당 팀에 속한 멤버 엔티티를 가져오는 쿼리문이니

멤버 엔티티를 담기 위한 멤버 타입의 리스트를 선언한다.

 

해당 리스트에 엔티티 매니저createQuery 메서드를 사용하여

기존에 만들어둔 쿼리문을 넘겨주고 반환 타입을 지정해준다.

 

쿼리문의 :teamName 파라미터에 값을 넘겨주기 위해

setParameter 메서드를 사용하여

어느 파라미터에 어떤 값을 넘겨줄지 지정해준다.

 

getResultList 메서드를 사용하여 쿼리문의 결과를

리스트 형태로 가져온다.

수정

Team team2 = new Team(2, "팀2");
em.persist(team2);

Member member = em.find(Member.class, 1);
member.setTeam(team2);

새로운 팀을 만들고 기존에 팀1에 속해있던 member_id가 1인 멤버를 팀2로 수정한다.

 

영속성 컨텍스트에서 Setter를 통해 엔티티의 값을 수정하면

변경 감지 기능이 자동으로 update 쿼리문을 생성해주기 때문에

위와 같이 간단한 작업만으로 수정할 수 있다.

제거

Member member = em.find(Member.class, 1);
member.setTeam(null);

연관관계 제거도 수정과 마찬가지로 기존의 연관관계를 null로 바꿔주기만 하면 된다.

연관된 엔티티 삭제

member1.setTeam(null);
member2.setTeam(null);
em.remove(team);

연관된 엔티티를 삭제하려면 기존에 있던 연관관계들을 모두 제거해야 한다.

 

만약 팀1을 삭제하려면 위의 코드처럼

팀1에 속한 멤버와의 연간관계를 모두 제거한 후에

해당 팀을 삭제해야 외래 키 제약조건에 걸리지 않고

연관된 엔티티를 삭제할 수 있다.

 

양방향 연관관계

양방향 연관관계 매핑

게시글 초반에 언급했던 것처럼 양방향 연관관계를 맺으려면

다른 반대 쪽에도 단방향 연관관계를 추가해주면 된다.

@Entity
@Table
@Getter @Setter
public class Team {
	@Id
    @Column(name = "team_id")
	private int Id;
    
    @Column(name = "team_name")
    private String teamName;
    
    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<Member>();
}

멤버는 여러 멤버가 하나의 팀에 속하는 것이니 @ManyToOne 어노테이션을 사용했다면

팀은 하나의 팀이 여러 멤버를 가지니 @OneToMany 어노테이션을 사용한다.

 

또한 멤버는 하나의 팀만 가지니 팀 객체를 하나만 가지는 필드를 선언했다면

팀은 여러 멤버를 가지는 리스트 필드를 선언한다.

일대다 컬렉션 조회

Team team = em.find(Team.class, 1);

List<Member> members = team.getMembers();

일대다 컬렉션도 마찬가지로 객체 그래프 탐색을 이용해서 조회한다.

 

연관관계의 주인

데이터베이스에서는 외래 키 하나 만으로 양방향 관계를 맺을 수 있지만

객체는 두 개의 단방향 관계를 사용하여 양방향 관계를 흉내내는 것만 가능하다.

 

하지만 객체를 양방향으로 만들면 참조는 둘인데 외래 키가 하나인 경우가 되기에

둘 사이에 차이가 발생하는 문제가 생긴다.

 

이를 해결하기 위해 두 객체의 연관관계 중 하나를 정해서

테이블의 연관관계를 관리하게 하고 이러한 역할을 연관관계의 주인이라 한다.

양방향 매핑 규칙

양방향 매핑을 하는 경우 두 연관관계 중 하나를 무조건 연관관계의 주인으로 정해야 한다.

 

연관관계의 주인만 데이터베이스 연관관계와 매핑되어 외래 키를 관리할 수 있게 만들고

나머지 반대 쪽은 읽기만 할 수 있게 만들어 위에서 언급한 차이를 없앤다.

public class Member {
	// 기존 코드 생략
	@ManyToOne
    @JoinColumn(name = "team_id")
    private Team team;
}

public class Team {
	// 기존 코드 생략
	@OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<Member>();
}

위의 코드처럼 연관관계의 주인인 Member 엔티티의 필드에는

mappedBy 속성을 사용하지 않고

 

주인이 아닌 Team 엔티티의 필드에는

mappedBy 속성을 사용하여 연관관계의 주인을 Member 엔티티의 team 필드로 지정한다.

 

즉, 연관관계의 주인은 테이블에 외래 키가 있는 곳으로 지정해야 한다.

 

멤버는 팀을 옮길 수 있으니 수정권한이 필요하지만

팀은 멤버를 옮길 수 없으니 수정권한이 필요 없다고 볼 수 있다.

 

다대일과 일대다 관계에서는 항상 다 쪽이 외래키를 가진다.

 

양방향 연관관계 저장

Team team1 = new Team(1, "팀1");
em.persist(team1);

Member member1 = new Member(1, "홍길동");
member1.setTeam(team1);
em.persist(member1);

위의 코드를 보면 단방향 연관관계에서의 코드와 완전히 일치하지만

이 코드가 양방향 연관관계를 저장하는 방식이다.

 

연관관계의 주인만 연관관계의 작업이 가능하기에

팀 엔티티가 아닌 멤버 엔티티를 통해 작업하는 것이다.

 

양방향 연관관계의 주의점

Member member1 = new Member(1, "홍길동");
em.persist(member1);

Team team1 = new Team(1, "팀1");
team1.getMembers().add(member1);
em.persist(team1);

위의 코드처럼 연관관계의 주인이 아닌 곳에서 값을 입력하는 경우에는

읽기전용이기 때문에 값이 추가되지 않는다.

public class Member {
	// 기존 코드 생략
	@ManyToOne
    @JoinColumn(name = "team_id")
    private Team team;
}

public class Team {
	// 기존 코드 생략
	@OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<Member>();
}

객체까지 고려해서 주인이 아닌 곳에도 값을 입력하는 것이 좋고,

객체의 양방향 연관관계는 반드시 양쪽 모두 관계를 맺어줘야 한다.

연관관계 편의 메서드

기존의 연관관계의 주인인 쪽의 Setter 메서드는 자신 쪽의 객체의 값만 수정하여

반대 쪽의 객체에는 값이 입력되지 않는다.

 

이러한 문제를 해결하기 위해 Setter를 수정하는데

이렇게 양쪽에 값을 추가하게 수정된 Setter를 편의 메서드라고 부른다.

 

수정된 코드는 아래와 같다.

public void SetTeam(Team team) {
	// 기존의 팀이 있다면 연관관계를 제거
	if(this.team != null) {
    	this.team.getMembers().remove(this);
    }
    // 주인뿐만 아니라 반대 쪽의 객체에도 값을 추가
    this.team = team;
    team.getMembers().add(this);
}

기존에 어떤 팀과 연관관계가 존재한다면 연관관계를 끊고

다른 팀과 연결해야하니 연관관계가 존재하는지 확인 후에

주인 쪽 객체에만 값을 추가하는 것이 아니라 양쪽 모두에 값을 추가한다.

 

정리

단방향 매핑만으로도 테이블과 객체의 연관관계 매핑은 완료되었기에

무조건 양방향 매핑을 할 필요는 없다.

 

양방향 매핑은 그저 반대 쪽에서도 객체 그래프 탐색 기능을

사용할 수 있다는 점을 빼면 장점이 없기 때문에

상황을 봐가면서 추가 하는 것이 좋다.

'Back-End > JPA' 카테고리의 다른 글

다양한 연관관계 매핑  (0) 2023.06.21
엔티티 매핑 : 학습을 위한 스키마 자동 생성하기  (0) 2023.06.19
엔티티 매핑 : 매핑 어노테이션  (0) 2023.06.19
영속성과 엔티티  (0) 2023.06.01
JPA 시작하기  (0) 2023.06.01
728x90

JPA는 클래스의 매핑정보와 데이터베이스 방언을 사용해

스키마를 자동으로 생성하는 기능을 지원한다.

<property name="hibernate.hbm2ddl.auto" value="create" />

해당 속성을 추가하면 애플리케이션 실행 시

데이터베이스에 테이블을 자동으로 생성해준다.

 

실제 서버 운영에서 사용할 만한 기능은 아니기 때문에

개발 환경이나 학습용으로만 참고하는 것이 좋다.

<property name="hibernate.show_sql" value="true" />

해당 속성을 추가하여 sql이 어떻게 작성되는지 볼 수 있다.

속성

create

기존 테이블 삭제 후 새로 생성

create-drop

create + 애플리케이션 종료 시 테이블 삭제

update

데이터베이스와 매핑정보를 비교해 변경 사항만 수정

validate

데이터베이스와 매핑정보를 비교해 차이가 있으면 경고 후 실행하지 않음

none

자동 생성 기능을 사용하지 않고 싶을 때 사용

 

'Back-End > JPA' 카테고리의 다른 글

다양한 연관관계 매핑  (0) 2023.06.21
연관관계 매핑 기초  (0) 2023.06.20
엔티티 매핑 : 매핑 어노테이션  (0) 2023.06.19
영속성과 엔티티  (0) 2023.06.01
JPA 시작하기  (0) 2023.06.01
728x90

@Entity

@Entity
public class Member {  }

테이블과 매핑할 클래스는 해당 어노테이션을 필수로 붙여야 한다.

속성

name

기본값은 클래스명을 사용하며, 직접 지정도 가능하다.

주의사항

기본 생성자는 필수다.

final 클래스, enum/interface/inner 클래스에는 사용할 수 없다.

필드에도 final을 사용하면 안 된다.

 

@Table

@Entity
@Table(name = "Member", uniqueConstraints = {@UniqueConstraint (name = "NAME_AGE_UNIQUE", columnNames = {"NAME", "AGE"})})
public class Member {  }

엔티티와 매핑할 테이블을 지정하는 어노테이션이다.

생략 시 엔티티 이름을 테이블로 사용한다.

속성

name

매핑할 테이블의 이름을 지정하는 속성으로 기본값은 엔티티 이름을 사용

catalog

카탈로그 기능이 있는 데이터베이스에서 카탈로그를 매핑

schema

스키마 기능이 있는 데이터베이스에서 스키마를 매핑

uniqueConstraints

DDL 생성 시에 유니크 제약조건을 만든다. 2개 이상의 복합 유니크 제약 조건도 생성 가능

스키마 자동 생성 기능으로 DDL을 만들 때만 사용

@ID

기본 키를 매핑할 때 사용하는 어노테이션

기본 키 생성 전략

직접 할당

기본 키를 애플레이케션에서 직접 할당

@Id 어노테이션만 사용

@Id
@Column(name = "ID")
private String id;

자동 생성

대리 키를 사용하는 방식

@Id 어노테이션과 @GeneratedValue 어노테이션을 사용

  • IDENTITY : 기본 키 생성을 데이터베이스에 위임(MySQL, PostgreSQL, SQL Server에서 사용)
    • 만약 MySQL의 기본키 속성에 AUTO_INCREMENT가 할당되어 있다면 해당 속성을 사용
    • 데이터베이스에 값을 저장한 후에 기본 키를 알 수 있음
    • 기본 키 값을 얻어오기 위해 데이터베이스의 추가적인 조회가 필요
    • 영속 상태가 되기 위해선 식별자 값이 필요하기 때문에 쓰기 지연이 적용되지 않음
    • 기본적으로 통신 횟수가 2회
    • 하이버네이트는 JDBC3에 추가된 Statement.getGeneratedKey 메서드를 사용하여 한 번만 통신
@Id
@Column(name = "ID")
@GeneratedValue(startegy = GenerationType.IDENTITY)
private String id;
  • SEQUENCE : 데이터베이스 시퀀스를 사용해서 할당 (Oracle, PostgreSQL, DB2, H2)
    • 유일한 값을 순서대로 생성
    • 사용하기 위해 시퀀스를 매핑해야 함
    • IDENTITY와 비슷하지만 식별자를 조회한 후에 엔티티에 할당한다.
    • 그렇기 때문에 영속성 컨텍스트에 저장이 가능하다.
    • 영속성 컨텍스트를 사용할 수 있기 때문에 쓰기 지연 기능 사용 가능
    • 데이터베이스의 시퀀스를 조회하는 작업이 필요해서 통신 횟수가 2회다.
//SEQUENCE 전략 사용
@Entity
@SequenceGenerator ( //시퀀스 생성기 등록
	name = "BOARD_SEQ_GENERATOR", // 시퀀스 생성기 이름
    sequenceName = "BOARD_SEQ", // DB의 시퀀스 이름
    initialValue = 1, //시작값
    allocationSize = 1 //증가하는 범위, 기본값 50
    //catalog //카탈로그명
    //schema //스키마명
)
public class Board {
	@Id
    @GeneratedValue(startegy = GenerationType.SEQUENCE,
    				generator = "BOARD_SEQ_GENERATOR") //시퀀스 생성기 사용
    private long Id;
}
  • TABLE : 키 생성 테이블을 사용
    • 기본키의 이름과 값 컬럼을 가진 키 생성 전용 테이블을 생성하여 사용
    • 테이블을 사용하여 모든 데이터베이스에서 사용할 수 있음
    • 테이블 전략 조회와 값을 증가시키기 위한 작업 때문에 통신 횟수가 1회 더 많다.
sequence_name == pkColumnName next_val == valueColumnName
BOARD_SEQ == pkColumnValue 2 == valueColumnValue
MEMBER_SEQ 10
PRODUCT_SEQ 50
//TABLE 전략 사용
@Entity
@SequenceGenerator ( //테이블 시퀀스 생성기 등록
	name = "BOARD_SEQ_GENERATOR", //테이블 시퀀스 생성기 이름
    sequenceName = "MY_SEQUENCE", //DB의 시퀀스 테이블 이름
    pkColumnValue = "BOARD_SEQ", //키로 사용할 값 이름
    allocationSize = 1 //증가하는 범위, 기본값 50
)
public class Board {
	@Id
    @GeneratedValue(startegy = GenerationType.TABLE,
    				generator = "BOARD_SEQ_GENERATOR") //테이블 시퀀스 생성기 사용
    private long Id;
}
  • AUTO : 선택한 데이터베이스에 따라 위의 3가지 전략 중 하나를 자동으로 선택
    • 오라클이면 SEQUENCE, MySQL이면 IDENTITY
    • 데이터베이스를 변경해도 코드를 수정하지 않아도 상관 없음
    • 키 생성 전략이 확정되지 않은 개발 초기 및 프로토타입 개발 시 유용
    • 스키마 자동 생성 기능을 사용하면 시퀀스나 키 생성용 테이블을 하이버네이트가 만들어 줌

기본키의 조건

  • null 허용하지 않음
  • 유일해야 함
  • 변하지 않아야 함

자연 키

비즈니스에 의미가 있는

사용자의 ID, 주민등록번호, 이메일 등

 

대리 키 (대체 키)

비스니스와 관련이 없는

오라클 시퀀스, auto_increment, 키 생성 테이블 등

 

비즈니스 규칙은 생각보다 쉽게 변하기 때문

자연 키는 기본키의 조건을 만족하지 않게될 수도 있어서

대리 키를 권장

@Column

@Column(name = "NAME", nullable = false, length = 10)
private String username;

컬럼을 매핑할 때 사용하는 어노테이션으로

옵션을 사용해 DDL을 생성할 수 있다.

속성

name

매핑할 컬럼명을 지정

기본값은 변수명을 따라감

nullable

null 값을 허용할지 지정

기본값은 true(허용)

length

문자의 최대 길이를 지정

insertable

해당 필드를 데이터베이스에 저장할지 지정

false면 데이터베이스에 저장하지 않으며 읽기 전용일 때 사용

기본값은 true

updatable

수정 내용을 데이터베이스에 반영할지 지정

insertable과 마찬가지로 읽기 전용일 때 사용

기본값은 true

table

하나의 엔티티를 두 개 이상의 테이블에 매핑할 때 사용

unique

하나의 컬럼에 유니크 제약조건을 걸 때 사용

두 개 이상의 컬럼에 유니크 제약조건을 걸려면

@Table 어노테이션의 uniqueConstraints 옵션을 사용

columnDefinition

데이터베이스의 컬럼 정보에 맞게 자동으로 설정

precision, scale

BigDecimal 타입에서 사용하며

각각 소수점 포함 전체 자릿수, 소수의 자릿수를 지정

double, float 타입에는 적용되지 않음

아주 큰 숫자나 정밀한 소수를 다루는 경우에만 사용

 

주의사항

@Column 어노테이션을 생략 시에

자바 기본 타입(int, char 등)은 null을 허용하지 않게 설정되고

객체 타입(Integer, String 등)은 null을 허용한다.

@Enumerated

enum RoleType {
	ADMIN, USER
}
@Enumerated(EnumType.STRING)
private RoleType roleType;

enum 타입을 매핑할 때 사용하는 어노테이션

속성

value

EnumType.ORDINAL : enum 순서를 데이터베이스에 저장  (기본값)

EnumType.STRING : enum 이름을 데이터베이스에 저장  (권장)

순서와 이름

위의 enum 클래스에는 순서대로 ADMIN, USER가 정의되어 있다.

 

ORDINAL 속성을 사용하면 각각 0, 1번이 되는데

데이터베이스에 저장되는 크기는 작지만,

이미 저장된 ENUM의 순서를 변경할 수 없다.

 

STRING 속성을 사용하면 각각의 이름을 그대로 사용한다.

저장된 ENUM의 순서가 바뀌거나 추가되어도 상관 없지만,

데이터베이스에 저장되는 크기가 크다.

@Temporal

@Temporal(Temporal.DATE)
private Date date;

@Temporal(Temporal.TIME)
private Date time;

@Temporal(Temporal.TIMESTAMP)
private Date timestamp;

Date, Calendar 같은 자바의 날짜 타입을 매핑할 때 사용하는 어노테이션

해당 어노테이션 생략 시 timestamp로 매핑한다.

속성

value(필수)

Temporal.DATE : 날짜를 데이터베이스의 date 타입과 매핑

Temporal.TIME : 시간을 데이터베이스의 time 타입과 매핑

Temporal.TIMESTAMP : 날짜와 시간을 데이터베이스의 timestamp 타입과 매핑

@Lob

@Lob
private String lobString //CLOB

@Lob
private byte[] lobByte //BLOB

문자열의 길이에 제한이 없는 경우 사용하는 BLOB, CLOB 타입으로 매핑할 때 사용하는 어노테이션

속성

지정할 수 있는 속성은 없지만

매핑하는 필드 타입에 따라 문자면 CLOB으로 매핑되고

나머지는 BLOB으로 매핑한다.

@Transient

@Transient
private Integer temp;

특정 필드를 매핑하지 않고 싶은 경우에 사용하는 어노테이션

 

데이터베이스에 저장되지도 않고 조회되지도 않으므로

객체에 임시로 데이터를 보관하고 싶을 때 사용

@Access

@Entity
@Access(AccessType.FIELD)
public class Member {
	@Id
	private String id;
}

@Entity
@Access(AccessType.PROPERTY)
public class Member {
	private String id;
    
    @Id
    public String getId() {
    	return id;
    }
}

JPA가 엔티티에 접근하는 방식을 지정할 때 사용하는 어노테이션

속성

AccessType

FIELD : 필드에 직접 접근하는 방식으로 접근 권한이 private이여도 가능

PROPERTY : 접근자(Getter)를 사용하여 접근하는 방식

 

기본키의 위치에 따라서 AccessType 속성을 적용한 것과 같기 때문에 생략해도 상관없다.

주의사항

nullable, length, uniqueConstraints rkxdms 속성은

DDL을 자동 생성할 때만 사용되고 JPA의 실행 로직에는 영향을 주지 않음

 

'Back-End > JPA' 카테고리의 다른 글

연관관계 매핑 기초  (0) 2023.06.20
엔티티 매핑 : 학습을 위한 스키마 자동 생성하기  (0) 2023.06.19
영속성과 엔티티  (0) 2023.06.01
JPA 시작하기  (0) 2023.06.01
엔티티 매핑 기본  (0) 2023.05.31

+ Recent posts