
์ ์๊ฐ ๊น์ง ์์ ๊ฐ๋จํ ํ์ ๊ฐ์ ๋ง ํด๋ณด์๋ค. ์ด๋ฒ์ ๋ก๊ทธ์ธ์ ํด๋ณด์
package com.shop.service;
import com.shop.entity.Member;
import com.shop.repository.MemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
// ์๋น์ค
@Service
// ํธ๋์ญ์
์ค์ : ์ฑ๊ณตํ๋ฉด ์ ์ฉ ์คํจํ๋ฉด ๋กค๋ฐฑ
@Transactional
// final ๋๋ @NonNull ๋ช
๋ น์ด๊ฐ ๋ถ์ผ๋ฉด ๊ฐ์ฒด๋ฅผ ์๋์ผ๋ก ๋ถ์ฌ์ค๋ค. @Autowired๊ฐ ํ์ ์๋ค๋ ๋ป
@RequiredArgsConstructor // ๋กฌ๋ณต ์ด๋
ธํ
์ด์
public class MemberService implements UserDetailsService { // implements? ๐ UserDetailsService๋ ์ธํฐํ์ด์ค ๐ ์ถ์ํ ๋ฉ์๋ ์ฌ์ ์
//@Autowired
//MemberRepository memberRepository;
private final MemberRepository memberRepository;
// ์ค๋ณต ๊ฒ์ฌ ํ์ ์์ผ๋ฉด ์ ์ฅ
public Member saveMember(Member member) {
validateDuplicateMember(member);
return memberRepository.save(member); // ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ ์ฅ์ ํ๋ผ๋ ๋ช
๋ น
}
// ์ด๋ฉ์ผ ์ค๋ณต ๊ฒ์ฌ ๋ฉ์๋
private void validateDuplicateMember(Member member) {
Member findMember = memberRepository.findByEmail(member.getEmail());
// Controller ์์ try/catch ๋ก ๋์ค๊ฒ
if (findMember != null) {
throw new IllegalStateException("์ด๋ฏธ ๊ฐ์
๋ ํ์์
๋๋ค.");
}
}
// UserDetailsService : ๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ํ์ ์ ๋ณด๋ฅผ ๊ฐ์ ธ์ค๋ ์ญํ ์ธํฐํ์ด์ค
// UserDetail : ์คํ๋ง ์ํ๋ฆฌํฐ์์ ํ์ ์ ๋ณด๋ฅผ ๋ด๊ธฐ ์ํด ์ฌ์ฉํ๋ ์ธํฐํ์ด์ค
// UserDetails๋ก usernameParameter("email")๋ฅผ ๊ฐ์ง๊ณ ๋ก๊ทธ์ธ ์ธ์ฆ.
// ์ฆ, ์ง์ ๊ตฌํํ๊ฑฐ๋ ์คํ๋ง ์ํ๋ฆฌํฐ์์ ์ ๊ณตํ๋ User ํด๋์ค ์ฌ์ฉ (๊ตฌํ์ฒด)
// loadUserByUsername() ๋ฉ์๋๋ฅผ ํตํด ํ์์ ๋ณด๋ฅผ ์กฐํ -> UserDetails ์ธํฐํ์ด์ค ๋ฐํ
@Override
public UserDetails loadUserByUsername(String email){
Member member = memberRepository.findByEmail(email);
if (member == null){
throw new UsernameNotFoundException(email);
}
//๋น๋ ํจํด
return User.builder().username(member.getEmail())
.password(member.getPassword())
.roles(member.getRole().toString())
.build();
}
}
package com.shop.config;
import com.shop.service.MemberService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
MemberService memberService;
// ์ ๋ถ ํ๋ฝ
// filterChain(HttpSecurity http) ๋ฉ์๋๋ฅผ ํตํด ๋ก๊ทธ์ธ ๋ฐ ๋ก๊ทธ์์ URL ์ง์
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
http.authorizeRequests(auth -> auth.requestMatchers("/", "/members/**").permitAll())
// http.formLogin() - http ๋ฅผ ํตํด ๋ค์ด์ค๋ form ๊ธฐ๋ฐ request ๋ฅผ ์ด์ฉํ์ฌ Login ์ ์ฒ๋ฆฌ
// form ํ๊ทธ์์ ์ฌ์ฉ์์ ID ๋ถ๋ถ์ default ๊ฐ์ผ๋ก "username" ํ๋
.formLogin(formLogin -> formLogin
.loginPage("/members/login") // ๋ก๊ทธ์ธ ํ์ด์ง
.defaultSuccessUrl("/") // ๋ก๊ทธ์ธ ์ฑ๊ณต ํ์ด์ง ๐ "/"
.usernameParameter("email")
.failureUrl("/members/login/error")) // ๋ก๊ทธ์ธ ์๋ฌ ํ์ด์ง
// ๋ก๊ทธ์์
.logout(logout -> logout
.logoutRequestMatcher(new AntPathRequestMatcher("/members/logout")) // ๋ก๊ทธ์์ ํ์ด์ง
.logoutSuccessUrl("/")); // ๋ก๊ทธ์์ ์ฑ๊ณต ํ์ด์ง ๐ "/"
return http.build();
}
// ๋น๋ฐ๋ฒํธ ์ํธํ
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
/*
AuthenticationManagerBuilder ๋ฅผ ํตํด AuthenticationManager ๋ฅผ ์์ฑํ์ฌ ์ธ์ฆ ์ฒ๋ฆฌ ์ํ
UserDetailsService ์ธํฐํ์ด์ค๋ฅผ ๊ตฌํํ๊ณ loadUserByUsername ๋ฉ์๋๋ฅผ ์ค๋ฒ๋ผ์ด๋ฉํ
memberService ๊ฐ์ฒด๋ฅผ ์ด์ฉํ์ฌ User ๊ฐ์ฒด๋ฅผ ์ป์ด๋ธ ๋ค,
์ง์ ๋ ๋น๋ฐ๋ฒํธ ์ํธํ ๋ฐฉ์์ผ๋ก ๋น๋ฐ๋ฒํธ๊ฐ ์ผ์นํ๋์ง ๊ฒ์ฆ
*/
@Autowired
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(memberService).passwordEncoder(passwordEncoder());
}
}
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layouts/layout1}">
<!-- ์ฌ์ฉ์ CSS ์ถ๊ฐ -->
<th:block layout:fragment = "css">
<style>
.error{
color: red;
}
</style>
</th:block>
<div layout:fragment = "content">
<form action="/members/login" role="form" method="post">
<div class = "form-group">
<label th:for = "email">์ด๋ฉ์ผ</label>
<input type="email" name="email" class="form-control" placeholder="์ด๋ฉ์ผ์ ์
๋ ฅํด์ฃผ์ธ์">
</div>
<div class = "form-group">
<label th:for = "password">๋น๋ฐ๋ฒํธ</label>
<input type="password" name="password" class="form-control" placeholder="๋น๋ฐ๋ฒํธ๋ฅผ ์
๋ ฅํด์ฃผ์ธ์">
</div>
<p th:if="${loginErrorMsg}" class="error" th:text="${loginErrorMsg}"></p>
<br>
<button class="btn btn-success">๋ก๊ทธ์ธ</button>
<button type="button" class="btn btn-warning" onclick="location.href='/members/new'">ํ์๊ฐ์
</button>
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
</form>
</div>
</html>
package com.shop.controller;
import com.shop.dto.MemberFormDto;
import com.shop.entity.Member;
import com.shop.service.MemberService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.Value;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;
@Controller
@RequestMapping("/members")
@RequiredArgsConstructor
public class MemberController {
private final MemberService memberService;
private final PasswordEncoder passwordEncoder;
// method = "get" ์ผ ๋ ์คํ
@GetMapping(value = "/new")
public String memberForm(Model model){
model.addAttribute("memberFormDto", new MemberFormDto());
return "member/memberForm";
}
// method = "post" ์ผ ๋ ์คํ DB์ ์ ์ฅ
@PostMapping(value = "/new")
// memberForm ์ค๋ฒ๋ก๋ฉ
public String memberForm(@Valid MemberFormDto memberFormDto, BindingResult bindingResult, Model model){
// ์๋ฌ๋๋ฉด ๋ค์ ๊ฐ์
if(bindingResult.hasErrors()){
return "member/memberForm";
}
try {
Member member = Member.createMember(memberFormDto, passwordEncoder);
memberService.saveMember(member);
}catch (IllegalStateException e){
model.addAttribute("errorMessage", e.getMessage());
return "member/memberForm";
}
return "redirect:/";
}
// Header์์ ๋๋ ์ ๋ ์คํ ๋์ด "/member/memberLoginForm"๋ก ๋ณด๋ธ๋ค. ๊ทธ๋์ GetMapping
// memberLoginForm ์์์ post๋ SecurityConfig .loginPage("/members/login") ์ผ๋ก ๊ฐ๋ค
// ๋ณต์ก....
@GetMapping(value = "/login")
public String loginMember(){
return "/member/memberLoginForm";
}
// ๋ก๊ทธ์ธ์ด ์คํจ ํ์ ๋ ์คํ
@GetMapping(value = "login/error")
public String loginError(Model model){
model.addAttribute("loginErrorMsg", "์์ด๋ ๋๋ ๋น๋ฐ๋ฒํธ๋ฅผ ํ์ธํด์ฃผ์ธ์");
return "/member/memberLoginForm";
}
}
๐ ์ถ๊ฐ
์คํ ํด๋ณด๋ฉด
org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'securityConfig': Requested bean is currently in creation: Is there an unresolvable circular reference?
์๋ฌ๊ฐ ๋ฐ์ ํ ํ ๋ฐ ์ ํ๋ฆฌ์ผ์ด์ ํ๋กํผํฐ์ ์ํ์ ์ถ๊ฐ ํด์ค์ผ ํ๋ค. ์๋ง ๋ฒ์ ์ด ๋ฐ๋๋ฉด์์ ๋ณ๊ฒฝ ๋ ๊ฒ์ผ๋ก ์ถ์ // ๋น๋ฐ๋ฒํธ ์ํธํ @Bean public static PasswordEncoder passwordEncoder(){ return PasswordEncoderFactories.createDelegatingPasswordEncoder(); }๐
SecurityConfig์์ ์ด๋ ๊ฒ ๋ณ๊ฒฝ ํด์ค์ผ ํ๋ค






๐ ๋ก๊ทธ์ธ ์คํจ

๐ ์ฑ๊ณต, ๋ก๊ทธ์์ ๊น์ง ์ ๋๋ค
1. memberLoginForm.html ๋ก๊ทธ์ธ
๐ ํด๋ฆญ
์ ์ํด
\
2. ๋ก๊ทธ์ธ์ ๋๋ ์ ๊ฒฝ์ฐ
๐ ์คํ ํ
๐ ์ธ์ฆ ์คํ
๐.usernameParameter("email")๋กselect์คํ
๐member email๊ฐ์ฒด๋ฅผ ํ๋(์ค๋ณต x) ๋ฐ๋๋ค.null์ด๋ฉด์๋ฌ throw
๐null์ด ์๋๋ฉดView์์ ๋ด๋ ค์จpassword๋ฅผUserDetails์ ๋น๊ตํด์ ํ์ธ.password๋ ์ด๋ฏธencoder๋ฅผ ํด๋์.
๐ ์ผ์นํ๋ค? ๋ก๊ทธ์ธ ์ฑ๊ณต
3. ๋ก๊ทธ์ธ ์คํจ ํ์ ๋
๐ ์คํ
JUnit์ผ๋ก ํ ์คํธํ๋ ์ต๊ด์ ๋ค์ฌ์ผ ํ๋ค
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
๐ ์์กด์ฑ ์ถ๊ฐ
package com.shop.controller;
import com.shop.dto.MemberFormDto;
import com.shop.entity.Member;
import com.shop.service.MemberService;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.domain.Page;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.transaction.annotation.Transactional;
import static org.junit.jupiter.api.Assertions.*;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.formLogin;
@SpringBootTest
// ๊ฐ์์ ์น์ ๋ง๋ค์ด ์ฃผ๋ mockMvc
@AutoConfigureMockMvc
@Transactional
@TestPropertySource(locations = "classpath:application-test.properties")
class MemberControllerTest {
@Autowired
private MemberService memberService;
// mockMvc : ์น ๋ธ๋ผ์ฐ์ ์ ์์ฒญ์ ํ๋ ๊ฒ ์ฒ๋ผ ํ
์ค๋ฅผ๋ฅผ ํ ์ ์๊ฒ ํ๋ ๊ฐ์ง ๊ฐ์ฒด
@Autowired
private MockMvc mockMvc;
@Autowired
PasswordEncoder passwordEncoder;
/*
1. memberFormDto ๊ฐ์ฒด๋ฅผ ์์ฑํด์ ๋๋ฏธ๋ฐ์ดํฐ์ ๋ฃ๋๋ค.
2. Member ๐ createMember๋ฅผ ํธ์ถํ๋๋ฐ
๋งค๊ฐ๋ณ์ memberFormDto, passwordEncoder๋ก Member ๊ฐ์ฒด ์์ฑ
3. memberService.saveMember ๐ Member๋ฅผ ํ
์ด๋ธ์ ์ ์ฅ
validateDuplicateMember๋ฅผ ์ด์ฉํด ๊ธฐ์กด ๊ฐ์
์ฌ๋ถ ํ์ธ
๊ฐ์
์ด ์๋ ๊ฒฝ์ฐ : memberRepository.save(member) ๋ก ํ
์ด๋ธ์ ์ ์ฅ
ํ
์ด๋ธ์ ๋ฐ์ดํฐ๊ฐ ์ ์ฅ์ด ๋๋ฉด ๋๊ฐ์ Member ๊ฐ์ฒด๋ฅผ ๋ฐํ
๊ฐ์
์ด ๋ ๊ฒฝ์ฐ : IllegalStateException("์ด๋ฏธ ๊ฐ์
๋ ํ์์
๋๋ค.")
*/
public Member createMember(String email, String password){
MemberFormDto memberFormDto = new MemberFormDto();
memberFormDto.setEmail(email);
memberFormDto.setPassword(password);
memberFormDto.setName("ํ๊ธธ๋");
memberFormDto.setAddress("์์ธ์ ๋งํฌ๊ตฌ ํฉ์ ๋");
memberFormDto.setTelNumber("010-1234-5678");
Member member = Member.createMember(memberFormDto, passwordEncoder);
return memberService.saveMember(member);
}
@Test
@DisplayName("๋ก๊ทธ์ธ ์ฑ๊ณต ํ
์คํธ")
public void loginSuccessTest() throws Exception {
String email = "test@email.com";
String password = "1234";
this.createMember(email, password); // ์ ๋ณ์๋ฅผ ๋ฃ์ด createMember๋ก DB์ ๋ฐ์ดํฐ ์ ์ฅ
// ๊ฐ์ ์น ์คํ ๐ ๋ก๊ทธ์ธ ์คํ userParameter("email")๋ฅผ ๊ฐ์ง๊ณ
// "/members/login" ์์ user : "test@email.com", password : "1234" ๋ฅผ
// mockMvc ๊ฐ์ ์น์์ authenticated()๋ก ์ธ์ฆ๋ ๊ฑธ๋ก ๊ธฐ๋ ํ๋ค. ์๋๋ฉด ํ
์คํธ ์คํจ
mockMvc.perform(formLogin().userParameter("email")
.loginProcessingUrl("/members/login")
.user(email)
.password(password))
.andExpect(SecurityMockMvcResultMatchers.authenticated());
}
}
@Test
@DisplayName("๋ก๊ทธ์ธ ์คํจ ํ
์คํธ")
public void loginFailedTest() throws Exception {
String email = "test@email.com";
String password = "1234";
this.createMember(email, password);
mockMvc.perform(formLogin().userParameter("email")
.loginProcessingUrl("/members/login")
.user(email)
.password("12345"))
// ๋ก๊ทธ์ธ ์ธ์ฆ์ด ๋์ง ์๊ธฐ๋ฅผ ๊ธฐ๋ ๐ ๋ก๊ทธ์ธ์ด ์คํจํ๊ธฐ๋ฅผ ์ํ๋ค
.andExpect(SecurityMockMvcResultMatchers.unauthenticated());
}
Thymeleaf Extras Springsecurity6
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity6</artifactId>
<version>3.1.2.RELEASE</version>
</dependency>
๐ ์์กด์ฑ ์ถ๊ฐ
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security" lang="ko"> ๐ sec ์ถ๊ฐ
<head>
...
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<!-- ADMIN๋ง ์ ๊ทผ ๊ฐ๋ฅ hasAnyAuthority('ROLE_ADMIN')-->
<li class="nav-item" sec:authorize="hasAnyAuthority('ROLE_ADMIN')">
<a class="nav-link" href="/admin/item/new">์ํ ๋ฑ๋ก</a>
</li>
<li class="nav-item" sec:authorize="hasAnyAuthority('ROLE_ADMIN')">
<a class="nav-link" href="/admin/items">์ํ ๊ด๋ฆฌ</a>
</li>
<!-- ๋ก๊ทธ์ธ์ด ๋ ๊ฒฝ์ฐ isAuthenticated -->
<li class="nav-item" sec:authorize="isAuthenticated()">
<a class="nav-link" href="/cart">์ฅ๋ฐ๊ตฌ๋</a>
</li>
<li class="nav-item" sec:authorize="isAuthenticated()">
<a class="nav-link" href="/orders">๊ตฌ๋งค์ด๋ ฅ</a>
</li>
<!-- ์๋ฌด๋ ์ ๊ทผ ๊ฐ๋ฅ isAnonymous-->
<li class="nav-item" sec:authorize="isAnonymous()">
<a class="nav-link" href="/members/login">๋ก๊ทธ์ธ</a>
</li>
<li class="nav-item" sec:authorize="isAuthenticated()">
<a class="nav-link" href="/members/logout">๋ก๊ทธ์์</a>
</li>
</ul>
...

๐ ๋ก๊ทธ์ธ๋ง ํ์


๐ ๋ณ๊ฒฝํ์ Apply

๐ USER ๋ก๊ทธ์ธ ์ฑ๊ณต


๐ ADMIN ๋ก๊ทธ์ธ ์ฑ๊ณต

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
layout:decorate="~{layouts/layout1}" lang="ko">
<!-- ์นํ ๋๋ ๋
์์ด๋๊น head, body๋ ํ์ ์๋ค -->
<div layout:fragment="content">
<h1>์ํ ๋ฑ๋ก ํ์ด์ง์
๋๋ค.</h1>
</div>
</html>

package com.shop.controller;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class ItemController {
@GetMapping(value = "/admin/item/new") // ADMIN ROLE ๋ง ๋ค์ด์ฌ ์ ์๋ค
public String itemForm(){
return "/item/itemForm";
}
}

package com.shop.config;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import java.io.IOException;
// AuthenticationEntryPoint ์ธํฐํ์ด์ค ๊ตฌํ ํด๋์ค
// ์ธ์ฆ๋์ง ์์ ์ฌ์ฉ์๊ฐ ๋ฆฌ์์ค ์์ฒญ ์ "Unauthorized" ์๋ฌ๋ฅผ ๋ฐ์์ํด
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authenticationException) throws IOException, ServletException {
// int SC_UNAUTHORIZED = 401; ๐ ์ฐ๋ฆฌ๊ฐ ์ธ์ฆ ์๋ฌ(401 ์๋ฌ)๋ฅผ, "Unauthorized"๋ก ๋ฐ๊พผ ๊ฒ
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
}
}
...
http.authorizeRequests(auth -> auth.requestMatchers("/", "/members/**", "/item/**" , "/images/**").permitAll() // ๋๊ตฌ๋ ์ ๊ทผ ๊ฐ๋ฅํ ํ์ด์ง
.requestMatchers("/css/**", "/js/**", "/img/**").permitAll() // static ํด๋ ์์ ์๋ /css, /js, /img ํ์ ๋ชจ๋ ํ์ผ์ ์ธ์ฆ ๋ฌด์
.requestMatchers("/admin/**").hasRole("ADMIN") // ADMIN๋ง ์ ๊ทผ ๊ฐ๋ฅํ ํ์ด์ง
.anyRequest().authenticated()) // ์์ ์กด์ฌํ๋ url patterns ๋ค์ ์ ์ธํ ๋๋จธ์ง ์์ฒญ๋ค
...
.logoutSuccessUrl("/")); // ๋ก๊ทธ์์ ์ฑ๊ณต ํ์ด์ง ๐ "/"
// ๊ถํ์ ๋ง์ง ์๋ ์ฌ์ฉ์๊ฐ ๋ฆฌ์์ค์ ์ ๊ทผํ ๋ ์ํ๋๋ ํธ๋ค๋ฌ
// ๊ถํ์ด ์๋ ์ฌ์ฉ์๊ฐ ๋ฆฌ์์ค๋ฅผ ์์ฒญํ๋ฉด "Unauthorized" ์๋ฌ ๋ฐ์
http.exceptionHandling(exceptHand -> exceptHand
.authenticationEntryPoint(new CustomAuthenticationEntryPoint()));
.authenticationEntryPoint(new CustomAuthenticationEntryPoint()));
return http.build();
...

๐ ํ์๊ฐ์

๐ http://localhost/admin/item/new ์ ์ ๊ทผ ๊ฐ๋ฅํ๋ค

๐ member.setRole(Role.USER); ๋ก ๋ฐ๊พธ๊ณ



๐ USER๋ item/new์ ์ ๊ทผ์ด ๋ถ๊ฐํ๋ค. requestMatchers("/admin/**").hasRole("ADMIN") ์ค์ ํ๊ธฐ ๋๋ฌธ
package com.shop.controller;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.transaction.annotation.Transactional;
import static org.junit.jupiter.api.Assertions.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@SpringBootTest
// ๊ฐ์์ ์น์ ๋ง๋ค์ด ์ฃผ๋ mockMvc
@AutoConfigureMockMvc
@TestPropertySource(locations = "classpath:application-test.properties")
class ItemControllerTest {
@Autowired
MockMvc mockMvc;
@Test
@DisplayName("์ํ ๋ฑ๋ก ํ์ด์ง ๊ถํ ํ
์คํธ")
// ํ์์ ์ด๋ฆ์ด admin์ด๊ณ role(๊ถํ)์ด ADMIN์ธ ์ ์ ๊ฐ ์ ๊ทผํ๋ ๊ฒ์ฒ๋ผ ๊ฐ์์ UserDetails ๊ฐ์ฒด ๊ตฌํ
@WithMockUser(username = "admin", roles = "ADMIN")
public void itemFormTest() throws Exception {
//๊ฐ์์ Get ๋ฐฉ์ URL Request ์์ฑ
// ์์ฒญ ๋น๋๋ฅผ get์ผ๋ก ๋ง๋ฆ "/admin/item/new" ์์ admin์ ๊ฑธ๋ฆฌ๊ฒ ๋๋ค.
mockMvc.perform(MockMvcRequestBuilders.get("/admin/item/new")) // get ์์ฒญ
.andDo(print()) // ์ฝ์์ฐฝ์ ์ถ๋ ฅ
.andExpect(status().isOk()); // ์๋ต ์ํ๊ฐ OK์ธ์ง ์ฒดํฌ
}
@Test
@DisplayName("์ํ ๋ฑ๋ก ํ์ด์ง ์ผ๋ฐ ํ์ ์ ๊ทผ ํ
์คํธ")
@WithMockUser(username = "user", roles = "USER") // ๊ฐ์์ role๊ณผ username ์ค์
public void itemFormNotAdminTest() throws Exception {
// ์์ฒญ ๋น๋๋ฅผ get์ผ๋ก ๋ง๋ฆ "/admin/item/new" ์์ admin์ ๊ฑธ๋ฆฌ๊ฒ ๋๋ค.
mockMvc.perform(MockMvcRequestBuilders.get("/admin/item/new")) // get ์์ฒญ
.andDo(print()) // Request & Response ๋ฉ์์ง ์ฝ์์ฐฝ์ ์ถ๋ ฅ
.andExpect(status().isForbidden()); // ์๋ต์ด Forbidden์ด ๋ฐ์ ๐ ๊ถํ ์๋ ์ฌ์ฉ์ ์ ๊ทผ 403 ์๋ฌ
}
}

๐ ADMIN, ์ฌ๋ฌ ์ ๋ณด๊ฐ ๋ด๊ฒจ์๋ค

๐ USER Status = 403, Error message = Forbidden ์๋ฌ
์ฐ๊ด ๊ด๊ณ: ํ ์ด๋ธ ์ธ๋ํค(FK) ์ฐ๊ฒฐ ๐ ์๋๋ฐฉ์ ๊ธฐ๋ณธํค(PK)
๋ณดํต์ 1:N ์ด๋ฉด N์ธ ๋ ์์ด ์ธ๋ํค๋ฅผ ๊ฐ๊ณ 1์ ๊ธฐ๋ณธํค๋ฅผ ์ฃผ๋ ์ญํ . ์๋ ๊ฒฝ์ฐ๋ ์๋ค
1:1 ๊ด๊ณ:@OnetoOne, ํ์๊ฐ์ ๊ณผ ํ์/์ ์ ์ ์ฅ๋ฐ๊ตฌ๋1:N ๊ด๊ณ:@OnetoMany, ๊ต์์ ํ์/์ฅ๋ฐ๊ตฌ๋์ ์ํN:1 ๊ด๊ณ:@ManytoOne, ์์ ๋ฐ๋N:M ๊ด๊ณ:@ManytoMany, ์ ์ ๋ค๊ณผ ์ํ๋จ๋ฐฉํฅ/์๋ฐฉํฅ
๋ฐ์ดํฐ๋ฒ ์ด์ค ์ค์ฌ ์ค๊ณํ ์ด๋ธ์์ ๊ด๊ณ๋ ํญ์์๋ฐฉํฅJPA ๊ฐ์ฒด์งํฅ ์ค์ฌ ์ค๊ณ์์๋๋จ๋ฐฉํฅ,์๋ฐฉํฅ์กด์ฌ
์ฅ๋ฐ๊ตฌ๋(cart)โถ์ ์ (member), ์ฅ๋ฐ๊ตฌ๋๊ฐ ์ ์ PK๋ฅผ ๊ฐ๊ณ ์๋ ์ํฉ
package com.shop.entity;
import jakarta.persistence.*;
import lombok.Data;
@Entity
@Table(name = "cart")
@Data
public class Cart {
@Id
@Column(name = "cart_id")
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@OneToOne // 1:1 ๋งคํ
@JoinColumn(name = "member_id") // JoinColumn ๋งคํ ํ ์ธ๋ํค ์ง์ . member_id ์ด๋ฆ ์ค์
//name์ ๋ช
์ํ์ง ์์ผ๋ฉด JPA๊ฐ ์์์ ID๋ฅผ ์ฐพ๊ธดํ์ง๋ง ์ํ๋ name์ด ์๋ ์๋ ์๋ค
private Member member;
}
Hibernate:
create table cart (
cart_id bigint not null,
member_id bigint,
primary key (cart_id)
) engine=InnoDB
...
Hibernate:
create table item (
price integer not null,
stock_number integer not null,
item_id bigint not null,
reg_time datetime(6),
update_time datetime(6),
item_nm varchar(50) not null,
item_detail tinytext not null,
item_sell_status enum ('SELL','SOLD_OUT'),
primary key (item_id)
) engine=InnoDB
...
Hibernate:
create table member (
member_id bigint not null,
address varchar(255),
email varchar(255),
name varchar(255),
password varchar(255),
tel_number varchar(255),
role enum ('USER','ADMIN'),
primary key (member_id)
) engine=InnoDB
...
Hibernate:
alter table cart
add constraint UK_7dds3r67nkhxm9sbs9r5obd46 unique (member_id)
Hibernate:
alter table member
add constraint UK_mbmcqelty0fbrvxp1q58dn57t unique (email)
Hibernate:
alter table cart
add constraint FKix170nytunweovf2v9137mx2o
foreign key (member_id)
references member (member_id)
๐ cart ํ
์ด๋ธ ์์ฑ ํ alter ๋ช
๋ น์ด๋ก ์ธ๋ํค(FK)๋ฅผ ๋ง์ง๋ง์ ์ง์ ํ๋ค
package com.shop.repository;
import com.shop.entity.Cart;
import org.springframework.data.jpa.repository.JpaRepository;
// ์ฅ๋ฐ๊ตฌ๋ ์กฐํ๋ฅผ ์ํ ์ฟผ๋ฆฌ๋ฌธ ๋ ๋ฆฌ๋ JpaRepository
public interface CartRepository extends JpaRepository<Cart, Long> {
}
package com.shop.entity;
import com.shop.dto.MemberFormDto;
import com.shop.repository.CartRepository;
import com.shop.repository.MemberRepository;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityNotFoundException;
import jakarta.persistence.PersistenceContext;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.test.context.TestPropertySource;
import org.springframework.transaction.annotation.Transactional;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
@Transactional
@TestPropertySource(locations = "classpath:application-test.properties")
class cartTest {
@Autowired
CartRepository cartRepository;
@Autowired
MemberRepository memberRepository;
@Autowired
PasswordEncoder passwordEncoder;
@PersistenceContext // Entity ์์์ฑ ์ ์ฅ ํ๊ฒฝ
EntityManager em;
public Member createMember(){
MemberFormDto memberFormDto = new MemberFormDto();
memberFormDto.setEmail("test@email.com");
memberFormDto.setName("ํ๊ธธ๋");
memberFormDto.setAddress("์์ธ์ ๋งํฌ๊ตฌ ํฉ์ ๋");
memberFormDto.setPassword("1234");
return Member.createMember(memberFormDto, passwordEncoder);
}
@Test
@DisplayName("์ฅ๋ฐ๊ตฌ๋ ํ์ ์ํฐํฐ ๋งคํ ์กฐํ ํ
์คํธ")
public void findCartAndMemberTest(){
Member member = createMember();
memberRepository.save(member);
Cart cart = new Cart();
cart.setMember(member);
cartRepository.save(cart);
em.flush(); // ์์์ฑ ์ปจํ
์คํธ์ ๋ฐ์ดํฐ๋ฅผ ์ ์ฅ ํ ํธ๋์ญ์
์ด ๋๋ ๋ flush() ํธ์ถํ์ฌ DB์ ๋ฐ์
em.clear(); // ์์์ฑ ์ปจํ
์คํธ์ ์กฐํ ํ ์ํฐํฐ๊ฐ ์์ ๊ฒฝ์ฐ ๋ฐ์ดํฐ ๋ฒ ์ด์ค๋ฅผ ์กฐํ, ์์์ฑ ์ปจํ
์คํธ๋ฅผ ๋น์ด๋ค
// Member ํ
์ด๋ธ์ ์ ์ฅ ๋๊ณ Cart ํ
์ด๋ธ์ ์ ์ฅ
/*
findById select * from cart where id = ?
์ ์ฟผ๋ฆฌ๋ฌธ์์ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๋ฉด EntityNotFoundException Throw
orElseThrow ๐ EntityNotFoundException(๊ฒฐ๊ณผ๊ฐ ์์๋)
์ ์ฟผ๋ฆฌ๋ฌธ์์ ๋ฌธ์ ๊ฐ ์์ผ๋ฉด Cart(Entity)
*/
Cart savedCart = cartRepository.findById(cart.getId()).orElseThrow(EntityNotFoundException::new);
// savedCart์ member_id์ member_id๊ฐ ๊ฐ๋ ๋น๊ต
assertEquals(savedCart.getMember().getId(), member.getId());
}
}
select
c1_0.cart_id,
m1_0.member_id,
m1_0.address,
m1_0.email,
m1_0.name,
m1_0.password,
m1_0.role,
m1_0.tel_number
from
cart c1_0
left join
member m1_0
on m1_0.member_id=c1_0.member_id
where
c1_0.cart_id=?
๐ left ์กฐ์ธ ์ฑ๊ณต, ์ฆ์ ๋ก๋ฉ์ผ๋ก Cart ์ํฐํฐ๋ฅผ ์กฐํํ ๋, ๋งคํ๋ ์ํฐํฐ๋ ํ ๋ฒ์ ์กฐํ(default) ๐ @OnetoOne(fetch = FetchType.EAGER) ์ด ๋ํดํธ๋ผ๋ ๊ฒ. ๋ง์ด ์ฐ๋ ๊ฒ์ ์ง์ฐ ๋ก๋ฉ LAZY. ๋์ค์ ๋์ฌ ์์ .
cart(์ฅ๋ฐ๊ตฌ๋) โฌ cart_item(์ฅ๋ฐ๊ตฌ๋ ์์ ์ํ) โก item (ํ๋งค ์ํ)
ํ๋์ ์ฅ๋ฐ๊ตฌ๋์ ์ฌ๋ฌ๊ฐ์ ์ํ์ ๋ด๋ ๊ฒ
package com.shop.entity;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
@Entity
@Getter
@Setter
@Table(name = "cart_item")
public class CartItem {
@Id
@GeneratedValue
@Column(name = "cart_item_id")
private Long id;
// 1 : N
@ManyToOne // CartItem : Cart, ํด๋์ค : ๋ณ์ ๐ ์ด๋ฐ ์์ผ๋ก ์๋ฉด ํธํ๋ค.
@JoinColumn(name = "cart_id")
private Cart cart;
// N : 1
@ManyToOne // CartItem : Item, ํด๋์ค : ๋ณ์ ๐ ์ด๋ฐ ์์ผ๋ก ์๋ฉด ํธํ๋ค.
@JoinColumn(name = "item_id")
private Item item;
// ์ํ ๊ฐ์ count
private int count;
}

๐ cart ํ
์ด๋ธ ์์ฑ

๐ cart_item ํ
์ด๋ธ ์์ฑ

๐ item ํ
์ด๋ธ ์์ฑ

๐ alter ๋ช
๋ น์ด๋ก ๊ฐ๊ฐ ์ธ๋ํค(FK) ์ง์
order โก member
ํ์ ํ๋ช ์ด ์ฃผ๋ฌธ์ ์ฌ๋ฌ๊ฐ ํ๋ค
package com.shop.constant;
public enum OrderStatus {
// ์ฃผ๋ฌธ, ์ทจ์. ํ๋ถ ๊ฐ์ ๊ฑด ๋์ค์
ORDER, CANCEL
}
package com.shop.entity;
import com.shop.constant.OrderStatus;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import javax.annotation.processing.Generated;
import java.time.LocalDateTime;
@Entity
@Table(name = "orders")
@Getter
@Setter
public class Order {
@Id
@GeneratedValue
@Column(name = "order_id")
private Long id;
@ManyToOne
@JoinColumn(name = "member_id")
private Member member;
private LocalDateTime orderDate;
@Enumerated(EnumType.STRING)
private OrderStatus orderStatus;
private LocalDateTime regTime;
private LocalDateTime updateTime;
}



๐ alter ๋ช
๋ น์ด๋ก ๊ฐ๊ฐ ์ธ๋ํค(FK) ์ง์



๐ ์ค๊ฐ์ ๋น๋ฐ๋ฒํธ ๋ฃ์ด์ฃผ๋ฉด ๋๋ค.


๐ ์ธ๋ํค

๐ ๊ธฐ๋ณธํค

๐ ํ ์ด๋ธ์ ๋๋ฌ๊ฐ๋ฉฐ ์ด๋ค ๊ฒ์ด ์ฌ์ฉ๋๋์ง ํ์ธ ํ ์ ์๋ค
member โ item
1:N โ N:1ํํ๋ก ๊ต์ฐจ ์ํฐํฐ๋ฅผ ์๋์ผ๋ก ๋ง๋ ๋ค. ์ด๋ฐ ์์ผ๋ก ์ฐ๊ฒฐ ๋ ํ ์ด๋ธ์๋ ์ปฌ๋ผ์ ์ถ๊ฐํ ์ ์์ด ์ฌ์ฉ ํ ์ผ์ ๋ง์ง ์๋ค.
private LocalDateTime updateTime; // ์์ ์๊ฐ
@ManyToMany
// ์ค๊ฐ ํ
์ด๋ธ์ ๋ฃ๋ ๊ณผ์
@JoinTable(
name = "member_item",
joinColumns = @JoinColumn(name = "member_id"),
inverseJoinColumns = @JoinColumn(name = "item_id")
)
private List<Member> member; // Member์์ ์ธ๋ํค๋ฅผ ๊ฐ์ ธ ์์ผ๋ Member๊ฐ ์ฃผ์ธ
}
์ํฐํฐ๋ฅผ ์๋ฐฉํฅ์ผ๋ก ์ฐ๊ด ๊ด๊ณ๋ฅผ ์ค์ ํ๋ฉด ๊ฐ์ฒด์ ์ฐธ์กฐ๋ ๋์ธ๋ฐ ์ธ๋ํค๋ ํ๋์ธ ๊ด๊ณ.
์ฆ, ์ธ๋ํค๋ฅผ ์๋ก ๊ฐ๊ณ ์๋ค ๐ ์ฃผ์ธ๊ณผ ๋ถํ ๊ด๊ณ๋ฅผ ์ ๋ฆฝํ๊ธฐ ์ฝ์ง ์๋ค.
Java์์๋List<>๋ฑ ์ ์ถ๊ฐํ๋ฉฐ ๊ด๊ณ๋ฅผ ๋ํ๋ด๋๋ฐDB์์๋ ๋ฐ๋ก ๋ํ๋ผ ๊ฒ์ด ์๊ณ ๊ทธ๋ฅ ๋ฃ์ผ๋ฉด ๋๋ค.์ฃผ์ธ๊ณผ ๋ถํ๋ฅผ ์ ํ๋ ๊ท์น
- ์ฐ๊ด ๊ด๊ณ์
์ฃผ์ธ(PK)์์ธ๋ํค(FK)๊ฐ ์๋ ๊ณณ์ผ๋ก ์ค์ - ์ฐ๊ด ๊ด๊ณ์
์ฃผ์ธ(PK)์ด์ธ๋ํค(FK)๋ฅผ ๊ด๋ฆฌ(๋ฑ๋ก, ์์ , ์ญ์ )- ๋ถํ๋ ์ฐ๊ด ๊ด๊ณ ๋งคํ ์
mappedBy์์ฑ ๊ฐ์ผ๋ก ์ฃผ์ธ ์ง์ .์ฝ๊ธฐ๋ง ๊ฐ๋ฅํ๋ค ๐ ์๋ก ์ธ๋ํค๋ฅผ ๊ฐ๊ณ ์์ผ๋ฏ๋กmappedBy๋ฅผ ๊ฐ๋ ์ชฝ์ด ๋ถํ
๐ ์ฃผ์ธ์mappedBy์์ฑ ์ฌ์ฉ โ, ๋ถํ๋mappedBy์์ฑ์ผ๋ก ์ฃผ์ธ ์ง์ โ๐
mappedBy๋ฅผ ์ ์ด์ฃผ๋ ์ด์ ๋ ๋ญ๊น? (by ๊น์ํ)๊ฐ์ฒด์ ํ ์ด๋ธ๊ฐ์ ์ฐ๊ด๊ด๊ณ๋ฅผ ๋งบ๋ ์ฐจ์ด๋ฅผ ์ดํดํด์ผ ํ๋ค
MEMBER๊ณผ TEAM์ ์์๋ก ๋ค์ด๋ณด๋ฉด
๊ฐ์ฒด ์ฐ๊ด ๊ด๊ณ๋
ํ์ โก ํ ์ฐ๊ด๊ด๊ณ 1๊ฐ (๋จ๋ฐฉํฅ)
ํ โก ํ์ ์ฐ๊ด๊ด๊ณ 1๊ฐ (๋จ๋ฐฉํฅ) ์ด๋ฉฐ
ํ ์ด๋ธ ์ฐ๊ด ๊ด๊ณ๋
ํ์ โ ํ ์ฐ๊ด๊ด๊ฒ๋ก ์๋ฐฉํฅ ์ด๋ค.์ฌ๊ธฐ์ ์ฐจ์ด์ ์
๊ฐ์ฒด์ ์๋ฐฉํฅ ๊ด๊ณ๋ ์ฌ์ค ์๋ฐฉํฅ ๊ด๊ณ๊ฐ ์๋๋ผ ์๋ก ๋ค๋ฅธ ๋จ๋ฐฉํฅ ๊ด๊ณ 2๊ฐ ์ด๋ค. ์ฆ,๊ฐ์ฒด๋ฅผ ์๋ฐฉํฅ์ผ๋ก ์ฐธ์กฐํ๋ ค๋ฉด๋จ๋ฐฉํฅ ์ฐ๊ด ๊ด๊ณ2๊ฐ๋ฅผ ๋ง๋ค์ด์ผ ํ๋ค(a.getB(),b.getA()๐ use ๊ด๊ณ )
ํ์ง๋งํ ์ด๋ธ์ ์๋ฐฉํฅ ๊ด๊ณ๋์ธ๋ํค ํ๋๋ก๋ ํ ์ด๋ธ์ ์ฐ๊ด ๊ด๊ณ๋ฅผ ๊ด๋ฆฌํ๋ค.MEMBER.TEAM_ID์ธ๋ํค ํ๋๋ก ์๋ฐฉํฅ ์ฐ๊ด ๊ด๊ณ๋ฅผ ๊ฐ๋๋ค. ์ฆ,์์ชฝ์ผ๋ก Join์ด ๊ฐ๋ฅํ๋ค.๊ทธ๋ผ ์ด๋ค ์ธ๋ํค๋ก ๊ด๋ฆฌ๋ฅผ ํด์ผํ ๊น?
TEAM.MEMBER_ID๋ก ๊ด๋ฆฌ๊ฐ ๊ฐ๋ฅํ ๊น? ๋ ์ค์ ๋ญ ๋ฏฟ์ด์ผํด?TEAM์ ์ ๋ฐ์ดํธ ํ๋๋ฐ ์ ์๋๋๊ฑฐ์ผ?? ์MEMBER๊ฐ ๋ฐ๋์ง?? ์๋ ์ ๊ฐ์ด ์๋ค์ด๊ฐ?? ๋ฐ๋๋กํ๋๊น ๊ฐ์ด ๋ค์ด๊ฐ๋๋ฐ?? ๋ฒ๊ทผ๊ฐ???๐ ๊ทธ๋์ ๊ท์น์ ํ๋ ์ ํ๋ค. ๋๋ค ๋์์ ๋ค์ด๊ฐ๋ฉด ์ฐ๊ด ๊ด๊ณ์ ์ฃผ์ธ๋ง ๊ด๋ฆฌ๋ฅผ ํ๋ค.
๐ ์ฃผ์ธ๊ณผ ๋ถํ๋ฅผ ์ ํ๋ ๊ท์น ์
mappedByํ๊ธฐ๋ก ํ ๊ฒโ๊ทธ๋์ ๋๊ตฌ๋ฅผ ์ฃผ์ธ์ผ๋ก ํด์ผํ๋๋ฐ?
๋ต์ 99% ์ ํด์ ธ ์๋ค. ์ธ๋ํค๋ฅผ ๊ฐ๊ณ ์๋ ๊ณณ์ ์ฃผ์ธ์ผ๋ก ์ ํ์. ์ธ์ง๋ถ์กฐํ๋ฅผ ์ฐจ๋จํ์.
๐ ์ฌ๊ธฐ์๋
Member.team์ด ์ฐ๊ด๊ด๊ณ์ ์ฃผ์ธ.
์ผ๋จ ๋จ๋ฐฉํฅ ๋งคํ์ผ๋ก ๊ฐ๋ฐ์ ํ๊ณ ์๋ฐฉํฅ์ด ํ์ํ ๋Java์ ๋ช์ค ์ถ๊ฐํด ์ค์(DB์๋ ์ํฅ์ด ์์ผ๋) ์ถ๊ฐํด ์ฃผ๋ฉด ๋๋ค.์ฆ, ๋จ๋ฐฉํฅ ๋งคํ๋ง์ผ๋ก๋ ORM ๋งคํ์ ๋๋ผ ์ ์๋ค. ์๋ฐฉํฅ ๋งคํ์ ๋จ์ํ๊ฒ ์กฐํํ๋ ๊ฒ์ ํธํ๊ฒ ํ๊ธฐ ์ํด ์ถ๊ฐํ๋ ๋ถ๊ฐ์ ์ธ ๊ธฐ๋ฅ์ด๋ผ ์๊ฐํ๋ฉด ๋๋ค.
๊ฐ์ ์ ๋ ฅํ ๋ ์ฃผ์ธ์ ๊ฐ์ ์ ๋ ฅํ๋ฉด ๋๋๋ฐ ์์ํ ๊ฐ์ฒด ๊ด๊ณ๋ฅผ ๊ณ ๋ คํ๋ค๋ฉด ํญ์
์์ชฝ ๋ค ๊ฐ์ ์ ๋ ฅํด์ผ ํ๋ค.์๋ฐฉํฅ ๋งคํ์ ์ฅ์
- ๋จ๋ฐฉํฅ ๋งคํ๋ง์ผ๋ก๋ ์ด๋ฏธ ์ฐ๊ด๊ด๊ณ ๋งคํ์ ์๋ฃ
- ์๋ฐฉํฅ ๋งคํ์ ๋ฐ๋ ๋ฐฉํฅ์ผ๋ก ์กฐํ(๊ฐ์ฒด ๊ทธ๋ํ ํ์) ๊ธฐ๋ฅ์ด ์ถ๊ฐ ๋ ๊ฒ ๋ฟ์ด๋ค
JPQL์์ ์ญ๋ฐฉํฅ์ผ๋ก ํ์ํ ์ผ์ด ๋ง๋ค- ๋จ๋ฐฉํฅ ๋งคํ์ ์ ํ๊ณ ์๋ฐฉํฅ์ ํ์ํ ๋ ์ถ๊ฐํด๋ ๋๋ค.
member โฌ orders โ order_item
๐ ํ ๋ช ์ ํ์์ ์ฌ๋ฌ ๋ฒ ์ฃผ๋ฌธ ๊ฐ๋ฅํ๋ฉฐ ํ ๋ฒ์ ์ฃผ๋ฌธ์ ์ฌ๋ฌ ๊ฐ์ ์ํ์ ์ฃผ๋ฌธ ๊ฐ๋ฅ
package com.shop.entity;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import java.time.LocalDateTime;
@Entity
@Getter
@Setter
// ํ
์ด๋ธ๋ช
์ด ์์ผ๋ฉด ์์์ ๋ง๋ฆ
public class OrderItem {
@Id
@GeneratedValue
@Column(name = "order_item_id")
private Long id;
@ManyToOne
@JoinColumn(name = "item_id") // ์ธ๋ํค
private Item item;
@ManyToOne
@JoinColumn(name = "order_id") // ์ธ๋ํค
private Order order;
private int orderPrice;
private int count;
private LocalDateTime regTime;
private LocalDateTime updateTime;
}
...
@ManyToOne
@JoinColumn(name = "member_id") // Order : Member
private Member member;
// ์๋ก ์ธ๋ํค๋ฅผ ๊ฐ๊ณ ์์ง๋ง
// mappedBy๋ก ์ฃผ์ธ/๋ถํ ๊ด๊ณ๋ฅผ ์ง์ . ์ฆ, order_item์ด ์ฃผ์ธ
// Join ์ปฌ๋ผ์ ์ฌ์ฉํ์ง ์๋๋ค
@OneToMany(mappedBy = "order") // order_item ํ
์ด๋ธ์ order ํ๋์ ๋งคํ
private List<OrderItem> orderItems = new ArrayList<>();
private LocalDateTime orderDate;
...
