: 매일 저녁, 하루를 마무리하며 작성 !
: ⭕ 지식 위주, 학습한 것을 노트 정리한다고 생각하고 작성하면서 머리 속 흩어져있는 지식들을 정리 !
class Solution {
public int solution(int[] a, int[] b) {
int answer = 0;
for(int i=0; i<a.length; i++){
answer += a[i]*b[i];
}
return answer;
}
}
// build.gradle : Security 추가
// Security
implementation 'org.springframework.boot:spring-boot-starter-security'
// Security 기능 제한
// SpringAuthApplication
package com.sparta.springauth;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;
@SpringBootApplication(exclude = SecurityAutoConfiguration.class) // Spring Security 인증 기능 제외
public class SpringAuthApplication {
public static void main(String[] args) {
SpringApplication.run(SpringAuthApplication.class, args);
}
}
@Configuration
public class PasswordConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
// 1. @Bean 설정된 메서드 호출
PasswordEncoder passwordEncoder = passwordConfig.passwordEncoder();
// 2. Spring IoC 컨테이너에 빈 (passwordEncoder) 저장
// passwordEncoder -> Spring IoC 컨테이너
'Bean' 이름: @Bean 이 설정된 메서드명 : public PasswordEncoder passwordEncoder() {..} → passwordEncoder
// 비밀번호를 암호화할 때 사용하는 PasswordEncoder의 구현체 BCryptPasswordEncoder를 Bean으로 수동등록 : PasswordConfig
package com.sparta.springauth.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
public class PasswordConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
// 등록한 passwordEncoder ‘Bean’을 사용하여 문자열을 암호화 : PasswordEncoderTest
package com.sparta.springauth;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.crypto.password.PasswordEncoder;
@SpringBootTest
public class PasswordEncoderTest {
@Autowired
PasswordEncoder passwordEncoder;
@Test
@DisplayName("수동 등록한 passwordEncoder를 주입 받아와 문자열 암호화")
void test1() {
String password = "Robbie's password";
// 암호화
String encodePassword = passwordEncoder.encode(password);
System.out.println("encodePassword = " + encodePassword);
String inputPassword = "Robbie";
// 복호화를 통해 암호화된 비밀번호와 비교
boolean matches = passwordEncoder.matches(inputPassword, encodePassword);
System.out.println("matches = " + matches); // 암호화할 때 사용된 값과 다른 문자열과 비교했기 때문에 false
}
}
// food > Food
package com.sparta.springauth.food;
public interface Food {
void eat();
}
// food > Chicken
package com.sparta.springauth.food;
import org.springframework.stereotype.Component;
@Component
public class Chicken implements Food {
@Override
public void eat() {
System.out.println("치킨을 먹습니다.");
}
}
// food > Pizza
package com.sparta.springauth.food;
import org.springframework.stereotype.Component;
@Component
public class Pizza implements Food {
@Override
public void eat() {
System.out.println("피자를 먹습니다.");
}
}
@SpringBootTest
public class BeanTest {
@Autowired
Food pizza;
@Autowired
Food chicken;
}
// Chicken 클래스에 @Primary를 추가
@Component
@Primary
public class Chicken implements Food {
@Override
public void eat() {
System.out.println("치킨을 먹습니다.");
}
}
// @Primary가 추가되면 같은 타입의 Bean이 여러 개 있더라도 우선 @Primary가 설정된 Bean 객체를 주입
@SpringBootTest
public class BeanTest {
@Autowired
Food food;
}
// Pizza 클래스에 @Qualifier("pizza") 를 추가
@Component
@Qualifier("pizza")
public class Pizza implements Food {
@Override
public void eat() {
System.out.println("피자를 먹습니다.");
}
}
// 주입하고자하는 필드에도 @Qualifier("pizza") 를 추가해주면 해당 Bean 객체가 주입
@SpringBootTest
public class BeanTest {
@Autowired
@Qualifier("pizza")
Food food;
}
// 같은 타입의 Bean들에 Qualifier와 Primary가 동시에 적용되어있다면 Qualifier의 우선순위가 더 높다.
@SpringBootTest
public class BeanTest {
@Autowired
@Qualifier("pizza")
Food food;
@Test
@DisplayName("Primary 와 Qualifier 우선순위 확인")
void test1() {
// 현재 Chicken 은 Primary 가 적용된 상태
// Pizza는 Qualifier 가 추가된 상태입니다.
food.eat();
}
}
쿠키-세션 방식의 인증 : 서버가 ‘특정 유저가 로그인 되었다’는 상태를 저장하는 방식
JWT 기반 인증 : 인증에 필요한 정보들을 암호화시킨 토큰을 의미
Application - Storage - Cookies 에 도메인 별로 저장되어 있는게 확인
// 쿠키 생성
public static void addCookie(String cookieValue, HttpServletResponse res) {
try {
cookieValue = URLEncoder.encode(cookieValue, "utf-8").replaceAll("\\+", "%20"); // Cookie Value 에는 공백이 불가능해서 encoding 진행
Cookie cookie = new Cookie(AUTHORIZATION_HEADER, cookieValue); // Name-Value
cookie.setPath("/");
cookie.setMaxAge(30 * 60);
// Response 객체에 Cookie 추가
res.addCookie(cookie);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e.getMessage());
}
}
// 쿠키 읽기
@GetMapping("/get-cookie")
public String getCookie(@CookieValue(AUTHORIZATION_HEADER) String value) {
System.out.println("value = " + value);
return "getCookie : " + value;
}
// auth > AuthController
package com.sparta.springauth.auth;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
@RestController
@RequestMapping("/api")
public class AuthController {
public static final String AUTHORIZATION_HEADER = "Authorization";
@GetMapping("/create-cookie")
public String createCookie(HttpServletResponse res) {
addCookie("Robbie Auth", res);
return "createCookie";
}
@GetMapping("/get-cookie")
public String getCookie(@CookieValue(AUTHORIZATION_HEADER) String value) {
System.out.println("value = " + value);
return "getCookie : " + value;
}
public static void addCookie(String cookieValue, HttpServletResponse res) {
try {
cookieValue = URLEncoder.encode(cookieValue, "utf-8").replaceAll("\\+", "%20"); // Cookie Value 에는 공백이 불가능해서 encoding 진행
Cookie cookie = new Cookie(AUTHORIZATION_HEADER, cookieValue); // Name-Value
cookie.setPath("/");
cookie.setMaxAge(30 * 60);
// Response 객체에 Cookie 추가
res.addCookie(cookie);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e.getMessage());
}
}
}
// createSession
@GetMapping("/create-session")
public String createSession(HttpServletRequest req) {
// 세션이 존재할 경우 세션 반환, 없을 경우 새로운 세션을 생성한 후 반환
HttpSession session = req.getSession(true);
// 세션에 저장될 정보 Name - Value 를 추가합니다.
session.setAttribute(AUTHORIZATION_HEADER, "Robbie Auth");
return "createSession";
}
// getSession
@GetMapping("/get-session")
public String getSession(HttpServletRequest req) {
// 세션이 존재할 경우 세션 반환, 없을 경우 null 반환
HttpSession session = req.getSession(false);
String value = (String) session.getAttribute(AUTHORIZATION_HEADER); // 가져온 세션에 저장된 Value 를 Name 을 사용하여 가져옵니다.
System.out.println("value = " + value);
return "getSession : " + value;
}
프로젝트 2개를 만들어서 Client 입장의 서버는 8080 port, Server 입장의 서버는 7070 port로 동시에 실행
// controller > RestTemplateController
package com.sparta.springresttemplateclient.controller;
import com.sparta.springresttemplateclient.dto.ItemDto;
import com.sparta.springresttemplateclient.service.RestTemplateService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("/api/client")
public class RestTemplateController {
private final RestTemplateService restTemplateService;
public RestTemplateController(RestTemplateService restTemplateService) {
this.restTemplateService = restTemplateService;
}
@GetMapping("/get-call-obj")
public ItemDto getCallObject(String query) {
return restTemplateService.getCallObject(query);
}
@GetMapping("/get-call-list")
public List<ItemDto> getCallList() {
return restTemplateService.getCallList();
}
@GetMapping("/post-call")
public ItemDto postCall(String query) {
return restTemplateService.postCall(query);
}
@GetMapping("/exchange-call")
public List<ItemDto> exchangeCall(@RequestHeader("Authorization") String token) {
return restTemplateService.exchangeCall(token);
}
}
// dto > ItemDto
package com.sparta.springresttemplateclient.dto;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Getter
@NoArgsConstructor
public class ItemDto {
private String title;
private int price;
}
// entity > User
package com.sparta.springresttemplateclient.entity;
import lombok.Getter;
@Getter
public class User {
private String username;
private String password;
public User(String username, String password) {
this.username = username;
this.password = password;
}
}
// service > RestTemplateService
package com.sparta.springresttemplateclient.service;
import com.sparta.springresttemplateclient.dto.ItemDto;
import org.springframework.stereotype.Service;
import java.util.List;
@Slf4j
@Service
public class RestTemplateService {
public ItemDto getCallObject(String query) {
return null;
}
public List<ItemDto> getCallList() {
return null;
}
public ItemDto postCall(String query) {
return null;
}
public List<ItemDto> exchangeCall(String token) {
return null;
}
}
// controller > ItemController
package com.sparta.springresttemplateserver.controller;
import com.sparta.springresttemplateserver.dto.ItemResponseDto;
import com.sparta.springresttemplateserver.dto.UserRequestDto;
import com.sparta.springresttemplateserver.entity.Item;
import com.sparta.springresttemplateserver.service.ItemService;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/server")
public class ItemController {
private final ItemService itemService;
public ItemController(ItemService itemService) {
this.itemService = itemService;
}
@GetMapping("/get-call-obj")
public Item getCallObject(@RequestParam String query) {
return itemService.getCallObject(query);
}
@GetMapping("/get-call-list")
public ItemResponseDto getCallList() {
return itemService.getCallList();
}
@PostMapping("/post-call/{query}")
public Item postCall(@PathVariable String query, @RequestBody UserRequestDto requestDto) {
return itemService.postCall(query, requestDto);
}
@PostMapping("/exchange-call")
public ItemResponseDto exchangeCall(@RequestHeader("X-Authorization") String token, @RequestBody UserRequestDto requestDto) {
return itemService.exchangeCall(token, requestDto);
}
}
// dto > UserRequestDto
package com.sparta.springresttemplateserver.dto;
import lombok.Getter;
@Getter
public class UserRequestDto {
private String username;
private String password;
}
// dto > ItemResponseDto
package com.sparta.springresttemplateserver.dto;
import com.sparta.springresttemplateserver.entity.Item;
import lombok.Getter;
import java.util.ArrayList;
import java.util.List;
@Getter
public class ItemResponseDto {
private final List<Item> items = new ArrayList<>();
public void setItems(Item item) {
items.add(item);
}
}
// entity > Item
package com.sparta.springresttemplateserver.entity;
import lombok.Getter;
@Getter
public class Item {
private String title;
private int price;
public Item(String title, int price) {
this.title = title;
this.price = price;
}
}
// service > ItemService
package com.sparta.springresttemplateserver.service;
import com.sparta.springresttemplateserver.dto.ItemResponseDto;
import com.sparta.springresttemplateserver.dto.UserRequestDto;
import com.sparta.springresttemplateserver.entity.Item;
import org.springframework.stereotype.Service;
import java.util.Arrays;
import java.util.List;
@Service
public class ItemService {
private final List<Item> itemList = Arrays.asList(
new Item("Mac", 3_888_000),
new Item("iPad", 1_230_000),
new Item("iPhone", 1_550_000),
new Item("Watch", 450_000),
new Item("AirPods", 350_000)
);
public Item getCallObject(String query) {
return null;
}
public ItemResponseDto getCallList() {
return null;
}
public Item postCall(String query, UserRequestDto requestDto) {
return null;
}
public ItemResponseDto exchangeCall(String token, UserRequestDto requestDto) {
return null;
}
}
// 1. RestTemplate을 주입
private final RestTemplate restTemplate;
// RestTemplateBuilder의 build()를 사용하여 RestTemplate을 생성합니다.
public RestTemplateService(RestTemplateBuilder builder) {
this.restTemplate = builder.build();
}
// 2. 요청 받은 검색어를 Query String 방식으로 Server 입장의 서버로 RestTemplate를 사용하여 요청
public ItemDto getCallObject(String query) {
// 요청 URL 만들기
URI uri = UriComponentsBuilder
.fromUriString("http://localhost:7070")
.path("/api/server/get-call-obj")
.queryParam("query", query)
.encode()
.build()
.toUri();
log.info("uri = " + uri);
ResponseEntity<ItemDto> responseEntity = restTemplate.getForEntity(uri, ItemDto.class);
log.info("statusCode = " + responseEntity.getStatusCode());
return responseEntity.getBody();
}
// 1. Server 입장의 서버에서 itemList를 조회하여 요청받은 검색어에 맞는 Item을 반환
public Item getCallObject(String query) {
for (Item item : itemList) {
if(item.getTitle().equals(query)) {
return item;
}
}
return null;
}
// 1. Item List 조회
public List<ItemDto> getCallList() {
// 요청 URL 만들기
URI uri = UriComponentsBuilder
.fromUriString("http://localhost:7070")
.path("/api/server/get-call-list")
.encode()
.build()
.toUri();
log.info("uri = " + uri);
ResponseEntity<String> responseEntity = restTemplate.getForEntity(uri, String.class);
log.info("statusCode = " + responseEntity.getStatusCode());
log.info("Body = " + responseEntity.getBody());
return fromJSONtoItems(responseEntity.getBody());
}
// 2. JSON 처리를 도와주는 라이브러리를 추가하여 받아온 JSON 형태의 String을 처리
public List<ItemDto> fromJSONtoItems(String responseEntity) {
JSONObject jsonObject = new JSONObject(responseEntity);
JSONArray items = jsonObject.getJSONArray("items");
List<ItemDto> itemDtoList = new ArrayList<>();
for (Object item : items) {
ItemDto itemDto = new ItemDto((JSONObject) item);
itemDtoList.add(itemDto);
}
return itemDtoList;
}
// 3. ItemDto에 받아온 JSONObject를 사용하여 초기화하는 생성자를 추가
@Getter
@NoArgsConstructor
public class ItemDto {
private String title;
private int price;
public ItemDto(JSONObject itemJson) {
this.title = itemJson.getString("title");
this.price = itemJson.getInt("price");
}
}
// 1. Server 입장의 서버에서 itemList를 ItemResponseDto에 담아 반환
public ItemResponseDto getCallList() {
ItemResponseDto responseDto = new ItemResponseDto();
for (Item item : itemList) {
responseDto.setItems(item);
}
return responseDto;
}
// 1. 요청 받은 검색어를 Query String 방식으로 Server 입장의 서버로 RestTemplate를 사용하여 요청
public ItemDto postCall(String query) {
// 요청 URL 만들기
URI uri = UriComponentsBuilder
.fromUriString("http://localhost:7070")
.path("/api/server/post-call/{query}")
.encode()
.build()
.expand(query)
.toUri();
log.info("uri = " + uri);
User user = new User("Robbie", "1234");
ResponseEntity<ItemDto> responseEntity = restTemplate.postForEntity(uri, user, ItemDto.class);
log.info("statusCode = " + responseEntity.getStatusCode());
return responseEntity.getBody();
}
// 1. Server 입장의 서버에서 itemList를 조회하여 요청받은 검색어에 맞는 Item을 반환
public Item postCall(String query, UserRequestDto userRequestDto) {
System.out.println("userRequestDto.getUsername() = " + userRequestDto.getUsername());
System.out.println("userRequestDto.getPassword() = " + userRequestDto.getPassword());
return getCallObject(query);
}
// 1. RestTemplate의 exchange를 사용
public List<ItemDto> exchangeCall(String token) {
// 요청 URL 만들기
URI uri = UriComponentsBuilder
.fromUriString("http://localhost:7070")
.path("/api/server/exchange-call")
.encode()
.build()
.toUri();
log.info("uri = " + uri);
User user = new User("Robbie", "1234");
RequestEntity<User> requestEntity = RequestEntity
.post(uri)
.header("X-Authorization", token)
.body(user);
ResponseEntity<String> responseEntity = restTemplate.exchange(requestEntity, String.class);
return fromJSONtoItems(responseEntity.getBody());
}
// 1. 전달된 header와 body의 정보를 확인
public ItemResponseDto exchangeCall(String token, UserRequestDto requestDto) {
System.out.println("token = " + token);
System.out.println("requestDto.getUsername() = " + requestDto.getUsername());
System.out.println("requestDto.getPassword() = " + requestDto.getPassword());
return getCallList();
}
API 이용 신청하기
사용할 API 살펴보기
// naver > controller > NaverApiController
package com.sparta.springresttemplateclient.naver.controller;
import com.sparta.springresttemplateclient.naver.dto.ItemDto;
import com.sparta.springresttemplateclient.naver.service.NaverApiService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("/api")
public class NaverApiController {
private final NaverApiService naverApiService;
public NaverApiController(NaverApiService naverApiService) {
this.naverApiService = naverApiService;
}
@GetMapping("/search")
public List<ItemDto> searchItems(@RequestParam String query) {
return naverApiService.searchItems(query);
}
}
// naver > dto > ItemDto
package com.sparta.springresttemplateclient.naver.dto;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.json.JSONObject;
@Getter
@NoArgsConstructor
public class ItemDto {
private String title;
private String link;
private String image;
private int lprice;
public ItemDto(JSONObject itemJson) {
this.title = itemJson.getString("title");
this.link = itemJson.getString("link");
this.image = itemJson.getString("image");
this.lprice = itemJson.getInt("lprice");
}
}
// naver > service > NaverApiService
package com.sparta.springresttemplateclient.naver.service;
import com.sparta.springresttemplateclient.naver.dto.ItemDto;
import lombok.extern.slf4j.Slf4j;
import org.json.JSONArray;
import org.json.JSONObject;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
@Slf4j(topic = "NAVER API")
@Service
public class NaverApiService {
private final RestTemplate restTemplate;
public NaverApiService(RestTemplateBuilder builder) {
this.restTemplate = builder.build();
}
public List<ItemDto> searchItems(String query) {
// 요청 URL 만들기
URI uri = UriComponentsBuilder
.fromUriString("https://openapi.naver.com")
.path("/v1/search/shop.json")
.queryParam("display", 15)
.queryParam("query", query)
.encode()
.build()
.toUri();
log.info("uri = " + uri);
RequestEntity<Void> requestEntity = RequestEntity
.get(uri)
.header("X-Naver-Client-Id", "Client-Id")
.header("X-Naver-Client-Secret", "Client-Secret")
.build();
ResponseEntity<String> responseEntity = restTemplate.exchange(requestEntity, String.class);
log.info("NAVER API Status Code : " + responseEntity.getStatusCode());
return fromJSONtoItems(responseEntity.getBody());
}
public List<ItemDto> fromJSONtoItems(String responseEntity) {
JSONObject jsonObject = new JSONObject(responseEntity);
JSONArray items = jsonObject.getJSONArray("items");
List<ItemDto> itemDtoList = new ArrayList<>();
for (Object item : items) {
ItemDto itemDto = new ItemDto((JSONObject) item);
itemDtoList.add(itemDto);
}
return itemDtoList;
}
}
프로젝트 생성
주문 APP DB table 설계
// create users table
create table users
(
id bigint not null auto_increment,
name varchar(255),
primary key (id)
);
// create food table
create table food
(
id bigint not null auto_increment,
name varchar(255),
price float(53) not null,
primary key (id)
);
// 주문한 음식의 정보를 파악하기 위해 food_id 컬럼을 추가
ALTER TABLE users ADD food_id bigint;
// 고객 테이블에 주문 정보를 입력
INSERT INTO users (name, food_id) VALUES ('Robbie', 1);
INSERT INTO users (name, food_id) VALUES ('Robbert', 1);
INSERT INTO users (name, food_id) VALUES ('Robbie', 2);
불필요하게 고객의 이름이 중복되는 문제가 발생
// 주문한 고객의 정보를 파악하기 위해 user_id 컬럼을 추가
ALTER TABLE food ADD user_id bigint;
// 음식 테이블에 주문 정보 입력
INSERT INTO food (name, price, user_id) VALUES ('후라이드 치킨', 15000, 1);
INSERT INTO food (name, price, user_id) VALUES ('후라이드 치킨', 15000, 2);
INSERT INTO food (name, price, user_id) VALUES ('양념 치킨', 20000, 1);
불필요하게 음식의 이름이 중복되는 문제가 발생
// 주문에 대한 정보를 기록할 orders 테이블을 추가
create table orders
(
id bigint not null auto_increment,
user_id bigint,
food_id bigint,
order_date date,
primary key (id)
);
// 주문 테이블을 사용하여 테이블들의 연관 관계를 해결
drop table if exists food;
drop table if exists users;
create table users
(
id bigint not null auto_increment,
name varchar(255),
primary key (id)
);
create table food
(
id bigint not null auto_increment,
name varchar(255),
price float(53) not null,
primary key (id)
);
alter table orders
add constraint orders_user_fk
foreign key (user_id)
references users (id);
alter table orders
add constraint orders_food_fk
foreign key (food_id)
references food (id);
INSERT INTO users (name) VALUES ('Robbie');
INSERT INTO users (name) VALUES ('Robbert');
INSERT INTO food (name, price) VALUES ('후라이드 치킨', 15000);
INSERT INTO food (name, price) VALUES ('양념 치킨', 20000);
INSERT INTO food (name, price) VALUES ('고구마 피자', 30000);
INSERT INTO food (name, price) VALUES ('아보카도 피자', 50000);
INSERT INTO orders (user_id, food_id, order_date) VALUES (1, 1, SYSDATE());
INSERT INTO orders (user_id, food_id, order_date) VALUES (2, 1, SYSDATE());
INSERT INTO orders (user_id, food_id, order_date) VALUES (2, 2, SYSDATE());
INSERT INTO orders (user_id, food_id, order_date) VALUES (1, 4, SYSDATE());
INSERT INTO orders (user_id, food_id, order_date) VALUES (2, 3, SYSDATE());
// 고객 ‘Robbie’가 주문한 음식 정보를 users 테이블 기준으로 조회
SELECT u.name as username, f.name as foodname, o.order_date as orderdate
FROM users u
INNER JOIN orders o on u.id = o.user_id
INNER JOIN food f on o.food_id = f.id
WHERE o.user_id = 1;
// food 테이블 기준으로 ‘Robbie’가 주문한 음식 정보를 조회
SELECT u.name as username, f.name as foodname, o.order_date as orderdate
FROM food f
INNER JOIN orders o on f.id = o.food_id
INNER JOIN users u on o.user_id = u.id
WHERE o.user_id = 1;
// 음식 Entity와 고객 Entity는 서로를 참조 : 양방향 관계
// 음식
@Entity
@Table(name = "food")
public class Food {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private double price;
@ManyToOne
@JoinColumn(name = "user_id")
private User user;
}
// 고객
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(mappedBy = "user")
private List<Food> foodList = new ArrayList<>();
}
// 반대로 단방향 관계
// 음식
@Entity
@Table(name = "food")
public class Food {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private double price;
@ManyToOne
@JoinColumn(name = "user_id")
private User user;
}
// 고객
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
}
- Entity에서 외래 키의 주인은 일반적으로 N(다)의 관계인 Entity 이지만 1 대 1 관계에서는 외래 키의 주인을 직접 지정
- 외래 키 주인만이 외래 키 를 등록, 수정, 삭제할 수 있으며, 주인이 아닌 쪽은 오직 외래 키를 읽기만 가능
- @JoinColumn()은 외래 키의 주인이 활용하는 애너테이션
// 음식
@Entity
@Table(name = "food")
public class Food {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private double price;
@OneToOne
@JoinColumn(name = "user_id")
private User user;
}
// 고객
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
}
// 음식
@Entity
@Table(name = "food")
public class Food {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private double price;
}
// 고객
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToOne
@JoinColumn(name = "food_id")
private Food food;
}
- 양방향 관계에서 외래 키의 주인을 지정해 줄 때 mappedBy 옵션을 사용
- 양방향이라면 외래 키의 주인은 상대 Entity 타입의 필드를 가지면서 @JoinColumn()을 활용하여 외래 키의 속성을 설정
- 상대 Entity는 외래 키의 주인 Entity 타입의 필드를 가지면서 mappedBy 옵션을 사용하여 속성 값으로 외래 키의 주인 Entity에 선언된 @JoinColumn()으로 설정되고 있는 필드명 입력
// 음식
@Entity
@Table(name = "food")
public class Food {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private double price;
@OneToOne
@JoinColumn(name = "user_id")
private User user;
}
// 고객
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToOne(mappedBy = "user")
private Food food;
}
// 음식
@Entity
@Table(name = "food")
public class Food {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private double price;
@OneToOne(mappedBy = "food")
private User user;
}
// 고객
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToOne
@JoinColumn(name = "food_id")
private Food food;
}
// OneToOneTest
package com.sparta.jpaadvance.relation;
import com.sparta.jpaadvance.repository.FoodRepository;
import com.sparta.jpaadvance.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;
@Transactional
@SpringBootTest
public class OneToOneTest {
@Autowired
UserRepository userRepository;
@Autowired
FoodRepository foodRepository;
}
// 단방향
// Food Entity
package com.sparta.jpaadvance.entity;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
@Entity
@Getter
@Setter
@Table(name = "food")
public class Food {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private double price;
@OneToOne
@JoinColumn(name = "user_id")
private User user;
}
// User Entity
package com.sparta.jpaadvance.entity;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
@Entity
@Getter
@Setter
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
}
// 단방향 테스트
@Test
@Rollback(value = false) // 테스트에서는 @Transactional 에 의해 자동 rollback 됨으로 false 설정해준다.
@DisplayName("1대1 단방향 테스트")
void test1() {
User user = new User();
user.setName("Robbie");
// 외래 키의 주인인 Food Entity user 필드에 user 객체를 추가해 줍니다.
Food food = new Food();
food.setName("후라이드 치킨");
food.setPrice(15000);
food.setUser(user); // 외래 키(연관 관계) 설정
userRepository.save(user);
foodRepository.save(food);
}
// 양방향
// User Entity
package com.sparta.jpaadvance.entity;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
@Entity
@Getter
@Setter
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToOne(mappedBy = "user")
private Food food;
public void addFood(Food food) {
this.food = food;
food.setUser(this);
}
}
// 양방향 테스트
@Test
@Rollback(value = false)
@DisplayName("1대1 양방향 테스트 : 외래 키 저장 실패")
void test2() {
Food food = new Food();
food.setName("고구마 피자");
food.setPrice(30000);
// 외래 키의 주인이 아닌 User 에서 Food 를 저장해보겠습니다.
User user = new User();
user.setName("Robbie");
user.setFood(food);
userRepository.save(user);
foodRepository.save(food);
// 확인해 보시면 user_id 값이 들어가 있지 않은 것을 확인하실 수 있습니다.
}
@Test
@Rollback(value = false)
@DisplayName("1대1 양방향 테스트 : 외래 키 저장 실패 -> 성공")
void test3() {
Food food = new Food();
food.setName("고구마 피자");
food.setPrice(30000);
// 외래 키의 주인이 아닌 User 에서 Food 를 저장하기 위해 addFood() 메서드 추가
// 외래 키(연관 관계) 설정 food.setUser(this); 추가
User user = new User();
user.setName("Robbie");
user.addFood(food);
userRepository.save(user);
foodRepository.save(food);
}
@Test
@Rollback(value = false)
@DisplayName("1대1 양방향 테스트")
void test4() {
User user = new User();
user.setName("Robbert");
Food food = new Food();
food.setName("고구마 피자");
food.setPrice(30000);
food.setUser(user); // 외래 키(연관 관계) 설정
userRepository.save(user);
foodRepository.save(food);
}
// 조회
@Test
@DisplayName("1대1 조회 : Food 기준 user 정보 조회")
void test5() {
Food food = foodRepository.findById(1L).orElseThrow(NullPointerException::new);
// 음식 정보 조회
System.out.println("food.getName() = " + food.getName());
// 음식을 주문한 고객 정보 조회
System.out.println("food.getUser().getName() = " + food.getUser().getName());
}
@Test
@DisplayName("1대1 조회 : User 기준 food 정보 조회")
void test6() {
User user = userRepository.findById(1L).orElseThrow(NullPointerException::new);
// 고객 정보 조회
System.out.println("user.getName() = " + user.getName());
// 해당 고객이 주문한 음식 정보 조회
Food food = user.getFood();
System.out.println("food.getName() = " + food.getName());
System.out.println("food.getPrice() = " + food.getPrice());
}
// 음식
@Entity
@Table(name = "food")
public class Food {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private double price;
@ManyToOne
@JoinColumn(name = "user_id")
private User user;
}
// 고객
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
}
// 음식
@Entity
@Table(name = "food")
public class Food {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private double price;
@ManyToOne
@JoinColumn(name = "user_id")
private User user;
}
// 고객
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(mappedBy = "user")
private List<Food> foodList = new ArrayList<>();
}
// ManyToOneTest
package com.sparta.jpaadvance.relation;
import com.sparta.jpaadvance.repository.FoodRepository;
import com.sparta.jpaadvance.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;
@Transactional
@SpringBootTest
public class ManyToOneTest {
@Autowired
UserRepository userRepository;
@Autowired
FoodRepository foodRepository;
}
// 단방향
// Food Entity
package com.sparta.jpaadvance.entity;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
@Entity
@Getter
@Setter
@Table(name = "food")
public class Food {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private double price;
@ManyToOne
@JoinColumn(name = "user_id")
private User user;
}
// User Entity
package com.sparta.jpaadvance.entity;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
@Entity
@Getter
@Setter
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
}
// 단방향 테스트
@Test
@Rollback(value = false)
@DisplayName("N대1 단방향 테스트")
void test1() {
User user = new User();
user.setName("Robbie");
Food food = new Food();
food.setName("후라이드 치킨");
food.setPrice(15000);
food.setUser(user); // 외래 키(연관 관계) 설정
Food food2 = new Food();
food2.setName("양념 치킨");
food2.setPrice(20000);
food2.setUser(user); // 외래 키(연관 관계) 설정
userRepository.save(user);
foodRepository.save(food);
foodRepository.save(food2);
}
// 양방향
// User Entity
package com.sparta.jpaadvance.entity;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import java.util.ArrayList;
import java.util.List;
@Entity
@Getter
@Setter
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(mappedBy = "user")
private List<Food> foodList = new ArrayList<>();
public void addFoodList(Food food) {
this.foodList.add(food);
food.setUser(this); // 외래 키(연관 관계) 설정
}
}
// 양방향 테스트
@Test
@Rollback(value = false)
@DisplayName("N대1 양방향 테스트 : 외래 키 저장 실패")
void test2() {
Food food = new Food();
food.setName("후라이드 치킨");
food.setPrice(15000);
Food food2 = new Food();
food2.setName("양념 치킨");
food2.setPrice(20000);
// 외래 키의 주인이 아닌 User 에서 Food 를 저장해보겠습니다.
User user = new User();
user.setName("Robbie");
user.getFoodList().add(food);
user.getFoodList().add(food2);
userRepository.save(user);
foodRepository.save(food);
foodRepository.save(food2);
// 확인해 보시면 user_id 값이 들어가 있지 않은 것을 확인하실 수 있습니다.
}
@Test
@Rollback(value = false)
@DisplayName("N대1 양방향 테스트 : 외래 키 저장 실패 -> 성공")
void test3() {
Food food = new Food();
food.setName("후라이드 치킨");
food.setPrice(15000);
Food food2 = new Food();
food2.setName("양념 치킨");
food2.setPrice(20000);
// 외래 키의 주인이 아닌 User 에서 Food 를 쉽게 저장하기 위해 addFoodList() 메서드 생성하고
// 해당 메서드에 외래 키(연관 관계) 설정 food.setUser(this); 추가
User user = new User();
user.setName("Robbie");
user.addFoodList(food);
user.addFoodList(food2);
userRepository.save(user);
foodRepository.save(food);
foodRepository.save(food2);
}
@Test
@Rollback(value = false)
@DisplayName("N대1 양방향 테스트")
void test4() {
User user = new User();
user.setName("Robbert");
Food food = new Food();
food.setName("고구마 피자");
food.setPrice(30000);
food.setUser(user); // 외래 키(연관 관계) 설정
Food food2 = new Food();
food2.setName("아보카도 피자");
food2.setPrice(50000);
food2.setUser(user); // 외래 키(연관 관계) 설정
userRepository.save(user);
foodRepository.save(food);
foodRepository.save(food2);
}
// 조회
@Test
@DisplayName("N대1 조회 : Food 기준 user 정보 조회")
void test5() {
Food food = foodRepository.findById(1L).orElseThrow(NullPointerException::new);
// 음식 정보 조회
System.out.println("food.getName() = " + food.getName());
// 음식을 주문한 고객 정보 조회
System.out.println("food.getUser().getName() = " + food.getUser().getName());
}
@Test
@DisplayName("N대1 조회 : User 기준 food 정보 조회")
void test6() {
User user = userRepository.findById(1L).orElseThrow(NullPointerException::new);
// 고객 정보 조회
System.out.println("user.getName() = " + user.getName());
// 해당 고객이 주문한 음식 정보 조회
List<Food> foodList = user.getFoodList();
for (Food food : foodList) {
System.out.println("food.getName() = " + food.getName());
System.out.println("food.getPrice() = " + food.getPrice());
}
}
// 음식
@Entity
@Table(name = "food")
public class Food {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private double price;
@OneToMany
@JoinColumn(name = "food_id") // users 테이블에 food_id 컬럼
private List<User> userList = new ArrayList<>();
}
// 고객
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
}
외래 키를 음식 Entity가 직접 가질 수 있다면 INSERT 발생 시 한번에 처리할 수 있지만 실제 DB에서 외래 키를 고객 테이블이 가지고 있기 때문에 추가적인 UPDATE가 발생된다는 단점이 존재
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToOne
@JoinColumn(name = "food_id", insertable = false, updatable = false)
private Food food;
}
// OneToManyTest
package com.sparta.jpaadvance.relation;
import com.sparta.jpaadvance.repository.FoodRepository;
import com.sparta.jpaadvance.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;
@Transactional
@SpringBootTest
public class OneToManyTest {
@Autowired
UserRepository userRepository;
@Autowired
FoodRepository foodRepository;
}
// 단방향
// Food Entity
package com.sparta.jpaadvance.entity;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import java.util.ArrayList;
import java.util.List;
@Entity
@Getter
@Setter
@Table(name = "food")
public class Food {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private double price;
@OneToMany
@JoinColumn(name = "food_id") // users 테이블에 food_id 컬럼
private List<User> userList = new ArrayList<>();
}
// User Entity
package com.sparta.jpaadvance.entity;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
@Entity
@Getter
@Setter
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
}
// 단방향 테스트
@Test
@Rollback(value = false)
@DisplayName("1대N 단방향 테스트")
void test1() {
User user = new User();
user.setName("Robbie");
User user2 = new User();
user2.setName("Robbert");
Food food = new Food();
food.setName("후라이드 치킨");
food.setPrice(15000);
food.getUserList().add(user); // 외래 키(연관 관계) 설정
food.getUserList().add(user2); // 외래 키(연관 관계) 설정
userRepository.save(user);
userRepository.save(user2);
foodRepository.save(food);
// 추가적인 UPDATE 쿼리 발생을 확인할 수 있습니다.
}
// 조회
@Test
@DisplayName("1대N 조회 테스트")
void test2() {
Food food = foodRepository.findById(1L).orElseThrow(NullPointerException::new);
System.out.println("food.getName() = " + food.getName());
// 해당 음식을 주문한 고객 정보 조회
List<User> userList = food.getUserList();
for (User user : userList) {
System.out.println("user.getName() = " + user.getName());
}
}