🐶 오늘 실습 내용
오늘은 로그인 기능을 구현하기 전에 지금까지 배운 내용을 토대로 사전 구축? 느낌으로 Member(회원)
, Product(상품)
, Orders(주문)
각각에 대한 CRUD 를 구현하는 시간을 가졌다.
주문은 회원과 상품을 다대다( N:M
) 관계를 맺으면서 생성되는 테이블로 회원이 할 수 있는 기능 중 하나이다. 따라서 주문 테이블에는 회원의 IDX 값과 상품의 IDX 값을 외래키로 갖게 된다.
따라서 이 3개의 테이블을 만들어 줄때는 회원과 주문을 1:N 관계, 양방향으로 설정해주고, 상품과 주문도 마찬가지로 1:N 관계, 양방향으로 설정 해줬다.
각자마다 작성하는 순서가 다르다고 하는데 나는 Entity - Repository - Dto - Service - Controller 이 순으로 작성하는게 이해도 잘되고 이제는 익숙해진것 같다.
아래는 내가 오늘 실습을 진행하면서 구현한 코드이다.
🐱 Member
// ✅ Member 엔티티
@Entity
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String email;
private String password;
@OneToMany(mappedBy = "member")
private List<Orders> orderList = new ArrayList<>();
}
// ✅ Member 레포지토리
@Repository
public interface MemberRepository extends JpaRepository<Member, Integer> {
public Optional<Member> findByEmail(String email);
public Optional<Member> findByPassword(String password);
}
findById
가 있는데 이것 외에 예를 들어, 내가 이메일 또는 패스워드로 찾아보겠다 할때는 저렇게 레포지토리에 해당 메서드를 만들어주면 서비스 클래스에서 사용이 가능하다.// ✅ Member Dto
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class MemberDto {
private Integer id;
private String email;
private String password;
private List<MemberOrdersDto> memberOrdersDtoList = new ArrayList<>();
}
// ✅ MemberService 클래스
@Service
public class MemberService {
private final MemberRepository memberRepository;
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
// 💻 CREATE
public void create(MemberDto memberDto) {
memberRepository.save(Member.builder()
.email(memberDto.getEmail())
.password(memberDto.getPassword())
.build());
}
// 💻 LIST
public List<MemberDto> list() {
List<Member> result = memberRepository.findAll();
List<MemberDto> memberDtos = new ArrayList<>();
for(Member member : result) {
List<MemberOrdersDto> memberOrdersDtos = new ArrayList<>();
List<Orders> ordersList = member.getOrderList();
for(Orders orders : ordersList) {
MemberOrdersDto memberordersDto = MemberOrdersDto.builder()
.id(orders.getId())
.productDto(ProductDto.builder()
.id(orders.getProduct().getId())
.name(orders.getProduct().getName())
.price(orders.getProduct().getPrice())
.build())
.build();
memberOrdersDtos.add(memberordersDto);
}
MemberDto memberDto = MemberDto.builder()
.id(member.getId())
.email(member.getEmail())
.password(member.getPassword())
.memberOrdersDtoList(memberOrdersDtos)
.build();
memberDtos.add(memberDto);
}
return memberDtos;
}
// 💻 READ
public MemberLoginRes read(String email, String password) {
Optional<Member> emailResult = memberRepository.findByEmail(email);
Optional<Member> pwResult = memberRepository.findByPassword(password);
if(emailResult.isPresent() && pwResult.isPresent()) {
Member member = emailResult.get();
return MemberLoginRes.builder()
.id(member.getId())
.email(member.getEmail())
.build();
} else {
return null;
}
}
// 💻 UPDATE
public void update(MemberDto memberDto) {
memberRepository.save(Member.builder()
.id(memberDto.getId())
.email(memberDto.getEmail())
.password(memberDto.getPassword())
.build()
);
}
// 💻 DELETE
public void delete(Integer id) {
memberRepository.delete(Member.builder().id(id).build());
}
}
// ✅ MemberController 클래스
@RestController
@RequestMapping("/member")
public class MemberController {
private final MemberService memberService;
public MemberController(MemberService memberService) {
this.memberService = memberService;
}
@RequestMapping(method = RequestMethod.POST, value = "/create")
public ResponseEntity create(@RequestBody MemberDto memberDto) {
memberService.create(memberDto);
return ResponseEntity.ok().body("ok");
}
@RequestMapping(method = RequestMethod.GET, value = "/list")
public ResponseEntity list() {
return ResponseEntity.ok().body(memberService.list());
}
@RequestMapping(method = RequestMethod.GET, value = "/read")
public ResponseEntity read(String email, String password) {
return ResponseEntity.ok().body(memberService.read(email, password));
}
@RequestMapping(method = RequestMethod.PATCH, value = "/update")
public ResponseEntity update(MemberDto memberDto) {
memberService.update(memberDto);
return ResponseEntity.ok().body("수정");
}
@RequestMapping(method = RequestMethod.DELETE, value = "/delete")
public ResponseEntity delete(Integer id) {
memberService.delete(id);
return ResponseEntity.ok().body("삭제");
}
}
🐻 Product
// ✅ Product 엔티티
@Entity
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String name;
private Integer price;
@OneToMany(mappedBy = "product")
private List<Orders> orderList = new ArrayList<>();
}
// ✅ Product 레포지토리
@Repository
public interface ProductRepository extends JpaRepository<Product, Integer> {
}
// ✅ Product Dto
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class ProductDto {
private Integer id;
private String name;
private Integer price;
}
// ✅ Product 서비스 클래스
@Service
public class ProductService {
private final ProductRepository productRepository;
public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
// 💻 CREATE
public void create(ProductDto productDto) {
productRepository.save(Product.builder()
.name(productDto.getName())
.price(productDto.getPrice())
.build());
}
// 💻 LIST
public List<ProductDto> list() {
List<Product> result = productRepository.findAll();
List<ProductDto> productDtos = new ArrayList<>();
for(Product product : result) {
ProductDto productDto = ProductDto.builder()
.id(product.getId())
.name(product.getName())
.price(product.getPrice())
.build();
productDtos.add(productDto);
}
return productDtos;
}
// 💻 READ
public ProductDto read(Integer id) {
Optional<Product> result = productRepository.findById(id);
if(result.isPresent()) {
Product product = result.get();
return ProductDto.builder()
.id(product.getId())
.name(product.getName())
.price(product.getPrice())
.build();
} else {
return null;
}
}
// 💻 UPDATE
public void update(ProductDto productDto) {
productRepository.save(Product.builder()
.id(productDto.getId())
.name(productDto.getName())
.price(productDto.getPrice())
.build()
);
}
// 💻 DELETE
public void delete(Integer id) {
productRepository.delete(Product.builder().id(id).build());
}
}
// ✅ ProductController 클래스
@RestController
@RequestMapping("/product")
public class ProductController {
private final ProductService productService;
public ProductController(ProductService productService) {
this.productService = productService;
}
@RequestMapping(method = RequestMethod.POST, value = "/create")
public ResponseEntity create(ProductDto productDto) {
productService.create(productDto);
return ResponseEntity.ok().body("상품 생성");
}
@RequestMapping(method = RequestMethod.GET, value = "/list")
public ResponseEntity list() {
return ResponseEntity.ok().body(productService.list());
}
@RequestMapping(method = RequestMethod.GET, value = "/read")
public ResponseEntity read(Integer id) {
return ResponseEntity.ok().body(productService.read(id));
}
@RequestMapping(method = RequestMethod.PATCH, value = "/update")
public ResponseEntity update(ProductDto productDto) {
productService.update(productDto);
return ResponseEntity.ok().body("상품 수정");
}
@RequestMapping(method = RequestMethod.DELETE, value = "/delete")
public ResponseEntity delete(Integer id) {
productService.delete(id);
return ResponseEntity.ok().body("상품 삭제");
}
}
🐮 Orders
// ✅ Orders 엔티티
@Entity
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Orders {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "Member_id")
private Member member;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "Product_id")
private Product product;
}
// ✅ Orders 레포지토리
@Repository
public interface OrdersRepository extends JpaRepository<Orders, Integer> {
}
// ✅ Orders Dto
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class OrdersDto {
private Integer id;
private MemberDto memberDto;
private ProductDto productDto;
}
// ✅ Orders 서비스 클래스
@Service
public class OrdersService {
private final OrdersRepository ordersRepository;
public OrdersService(OrdersRepository orderRepository) {
this.ordersRepository = orderRepository;
}
// 💻 CREATE
public void create(Integer memberid, Integer productid, OrdersDto orderDto) {
ordersRepository.save(Orders.builder()
.member(Member.builder().id(memberid).build())
.product(Product.builder().id(productid).build())
.build());
}
// 💻 LIST
public List<OrdersDto> list(){
List<Orders> result = ordersRepository.findAll();
List<OrdersDto> ordersDtos = new ArrayList<>();
for(Orders orders : result) {
Member member = orders.getMember();
Product product = orders.getProduct();
MemberDto memberDto = MemberDto.builder()
.id(member.getId())
.email(member.getEmail())
.password(member.getPassword())
.build();
ProductDto productDto = ProductDto.builder()
.id(product.getId())
.name(product.getName())
.price(product.getPrice())
.build();
OrdersDto ordersDto = OrdersDto.builder()
.id(orders.getId())
.memberDto(memberDto)
.productDto(productDto)
.build();
ordersDtos.add(ordersDto);
}
return ordersDtos;
}
// 💻 READ
public OrdersDto read(Integer idx) {
Optional<Orders> result = ordersRepository.findById(idx);
if(result.isPresent()) {
Orders orders = result.get();
return OrdersDto.builder()
.id(orders.getId())
.memberDto(MemberDto.builder()
.id(orders.getMember().getId())
.email(orders.getMember().getEmail())
.password(orders.getMember().getPassword())
.build())
.productDto(ProductDto.builder()
.id(orders.getProduct().getId())
.name(orders.getProduct().getName())
.price(orders.getProduct().getPrice())
.build())
.build();
} else {
return null;
}
}
// 💻 UPDATE
public void update(OrdersDto ordersDto) {
Optional<Orders> result = ordersRepository.findById(ordersDto.getId());
if(result.isPresent()) {
Orders orders = result.get();
ordersRepository.save(orders);
}
}
// 💻 DELETE
public void delete(Integer id) {
ordersRepository.delete(Orders.builder().id(id).build());
}
}
// ✅ OrdersController 클래스
@RestController
@RequestMapping("/orders")
public class OrdersController {
private final OrdersService ordersService;
public OrdersController(OrdersService ordersService) {
this.ordersService = ordersService;
}
@RequestMapping(method = RequestMethod.POST, value = "/create")
public ResponseEntity create(Integer memberid, Integer productid, OrdersDto ordersDto) {
ordersService.create(memberid, productid, ordersDto);
return ResponseEntity.ok().body("주문 작성 성공");
}
@RequestMapping(method = RequestMethod.GET, value = "/list")
public ResponseEntity list() {
return ResponseEntity.ok().body(ordersService.list());
}
@RequestMapping(method = RequestMethod.GET, value = "/read")
public ResponseEntity read(Integer id) {
return ResponseEntity.ok().body(ordersService.read(id));
}
@RequestMapping(method = RequestMethod.PATCH, value = "/update")
public ResponseEntity update(OrdersDto ordersDto) {
ordersService.update(ordersDto);
return ResponseEntity.ok().body("주문 수정 성공");
}
@RequestMapping(method = RequestMethod.DELETE, value = "/delete")
public ResponseEntity delete(Integer id) {
ordersService.delete(id);
return ResponseEntity.ok().body("주문 삭제 성공");
}
}
CRUD 기능을 구현하면서 들었던 생각 🧐
어제 Movie
와 Review
를 가지고 관계를 맺고, CRUD 기능을 구현하는 실습을 해봤었는데, 오늘은 사실 그거에 대한 연장선 느낌이었다.
결국 구현할 수 있는 관계는 대부분이 1:N
관계이기 때문에 CRUD를 구현하는 코드는 같을 수 밖에 없다. 다시한번 느끼는거지만 가장 중요한것은 코드를 짜는게 아니라 이 클래스들이 서로 어떻게 이어지는지 작동 과정을 이해하는게 정말 중요한 것 같다.
그것을 어느정도 이해하고 나니, 오늘 위의 코드를 구현하는 것은 어렵지 않았고, 오히려 생각하면서 응용하는것도 가능했다. 예를들어, 위에서 구현한 코드중 회원의 정보를 조회할때 그 회원이 주문한 상품 목록까지 출력되도록 하고 싶다는 생각을 하고, 그것을 그대로 코드에 구현했는데, 막힘없이 생각한대로 구현하니 내가 원하는 데이터가 반환되는 것을 보고 엄청 즐거웠다.
즉, 클라이언트로부터 어떤 데이터를 입력받고 그 데이터들이 어떤 과정을 거쳐서 어떤 형태로 반환되는지 이 원리를 알고 있다면, 나머지는 사실 응용하는 부분이 아닐까 싶다.
로그인 기능 실습 전 사전 연습 ✍
내일 본격적으로 로그인 기능을 배워볼 예정인데, 그전에 기존에 구현했던 Member 의 READ 기능을 조금 수정하여 로그인처럼 작성하는 실습을 추가로 실시하였다.
구현하고자 하는 것은 클라이언트가 이메일과 패스워드를 입력하여 로그인을 시도하면 DB에서 해당값이 있는지 확인하고
아래는 추가로 작성한 코드이다.
// ✅ MemberLoginReq
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class MemberLoginReq {
private String email;
private String password;
}
// ✅ MemberLoginRes
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class MemberLoginRes {
private Integer id;
private String email;
}
// ✅ MemberService 클래스에 작성
public MemberLoginRes login(MemberLoginReq memberLoginReq) {
Optional<Member> emailResult = memberRepository.findByEmail(memberLoginReq.getEmail());
if(emailResult.isPresent() && memberLoginReq.getPassword().equals(emailResult.get().getPassword())) {
Member member = emailResult.get();
return MemberLoginRes.builder()
.id(member.getId())
.email(member.getEmail())
.build();
} else {
return null;
}
}
// ✅ MemberController 클래스에 작성
@RequestMapping(method = RequestMethod.POST, value = "/login")
public ResponseEntity login(@RequestBody MemberLoginReq memberLoginReq) {
return ResponseEntity.ok().body(memberService.login(memberLoginReq));
}
(프론트엔드 수업때 배울 예정)
를 이용하여 실행시켜보니 아래와 같은 웹페이지가 등장했다.4시간 걸려서 해결해낸 주문기능... 😭
로그인까지 탄탄대로처럼 구현하고, 마지막으로 주문 기능을 테스트해보는데 암만해도 주문 기능 동작이 안되었다. 디버깅을 통해 안되는 원인을 보는데 분명히 결제하기 버튼을 클릭하면 주문 컨트롤러로 요청이 들어오는데 멤버id
와 상품id
가 null 로 들어오는 것이다.
그러다 보니 주문 테이블에 해당 데이터가 저장 자체가 안되어서 그런거였는데, 도저히 이유를 알아낼 수가 없었다. 챗 gpt, 구글링 등 모든 검색을 했지만 원인이 도저히 찾을수가 없었다.
그러다가 문득, 다시한번 구현한 것을 Postman으로 테스트 해보는데 기존에 잘만 되던 Postman에서도 요청을 보내면 똑같이 컨트롤러에서 멤버id
와 상품id
가 null로 들어오는 것이었다.
아래가 기존의 주문 생성과 관련된 DTO 클래스였는데, 멤버id와 상품id를 Member_id, Product_id
로 설정한 이유는 강사님이 만드신 프론트엔드 서버에서 요청을 보낼때 이러한 형식으로 보낸다고 하여 작성한 것이였다.
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class PostOrderReq {
private Integer Member_Id;
private Integer Product_Id;
}
그래서 기존에 잘되던 DTO 클래스와 비교를 해보니 변수명 말고는 다른게 전혀 없었다. 그래서 변수명을 memberId
, ProductId
로 변경해서 Postman으로 요청을 보내보니 정상적으로 주문이 생성되는 것을 볼 수 있었다.
기분이 좋아서 바로 웹페이지에서 테스트 해보는데... 역시나 웹페이지에서는 아직도 null 값으로 컨트롤러로 들어오고 있었다. 도대체 문제가 몰까? 고민끝에 구글링에 검색해볼 문장이 떠올랐다. 바로 http 요청이 컨트롤러로 null로 들어올때
이 문장이었다.
이렇게 검색하니 한 글에서 @RequestBody 어노테이션으로 받은 json 데이터가 비어있을때 확인해볼 내용이 적혀있었다.
먼저 @RequestBody는 간단하게 말해서 Http요청의 Body를 자바 객체로 매핑해주는 어노테이션이다. 즉, 전송받은 JSON 데이터를 객체로 변환해서 받도록 하는 어노테이션인데, 이것이 문제였던 것이다.
스프링에는 Jackson 라이브러리가 있는데, 이것은 JAVA에서 JSON 데이터를 다루기 위한 라이브러리 중 하나이다. 이 Jackson이 하는 역할은 JSON 데이터와 Java 객체 간의 변환을 지원하는 것으로, 스프링에서는 기본적으로 Jackso을 JSON 데이터 처리에 사용하고 있다. 그런데 이 Jackson 라이브러리가 기본적으로 사용하는 것이 바로
카멜 표기법이었던 것이다.
따라서 Java 클래스의 필드 이름이 카멜 표기법으로 되어 있어야지만, JSON 데이터의 키와 매핑이 이루어지는데 나는 스네이크 표기법으로 변수를 만들어서 매핑이 정상적으로 안되어서 null 값이 계속 들어왔던 것이었다.
하지만 Jackson은 이러한 상황까지 고려하여 다양한 어노테이션을 제공하여 필드와 JSON 키 간의 매핑을 직접 지정할 수 있도록 하였는데 그것이 바로 @JsonProperty
어노테이션이었다.
아래는 @JsonProperty 어노테이션을 사용하여 수정한 내용이다.
public class PostOrderReq {
@JsonProperty("Member_id")
private Integer memberId;
@JsonProperty("Product_id")
private Integer productId;
}
이렇게 설정해주니 컨트롤러로 정상적으로 멤버 id 와 제품 id가 들어왔고, 주문이 정상적으로 되서 아래와 같이 동작하였다.🥹
여기서 궁금증이 그러면 내가 지금까지 실습하면서 Postman 으로 요청을 보내고 응답을 받았을때 응답이 JSON 형태로 데이터가 반환이 되었는데 이것은 어떻게 한거지? 라는 생각이 들었다. 그것이 바로!!! @RestController의 역할이었던 것이다. @RestController에는 기존의 @Controller와 @ResponseBody 가 포함되어 있기때문에 @ResponseBody 어노테이션으로 인해 JSON 형태의 데이터로 반환이 되고 있던 것이다.
여기서 또 알아낸것은 그럼 우리가 컨트롤러에서 작성할때 ResponseEntity를 사용하는 이유는 모지? 라는 생각이 들었다. 이것은 @ResponseBody와 동일하게 HTTP 응답 메시지의 바디에 메시지를 설정한다는 점에서 동힐하지만, 이 둘에는 차이점이 있다. @ResponseBody는 HTTP 헤더를 설정하기가 어렵고, 상태 코드도 별도의 옵션을 줘야지 설정할 수 있다. 하지만 ResponseEntity는 HTTPEntity를 상속받고 있기 때문에 여러 HTTP 옵션을 별도의 작업없이 설정할 수 있어서 편리할 수 있지만 @ResoponseBody보다는 작성할 코드가 많다는 단점이 있다.
그러면 또 여기서 HTTP 헤더는 무엇일까? 란 의문을 품게 된다. 먼저 헤더란 저장 되거나 전송되는 데이터 블록의 맨 앞에 위치한 데이터를 가리키며, 특정 프로토콜의 헤더의 내용은 특정 프로토콜의 기능을 제공하기 위한 정보를 담고 있다. 헤더의 뒤에 이어지는 데이터는 페이로드 혹은 바디로 불린다. 따라서 HTTP 헤더는 클라이언트와 서버가 요청 또는 응답으로 부가적인 정보를 전송할 수 있도록 해주는 것이다.
마지막으로 @RequestBody를 달아주니 포스트맨에서는 또 요청이 제대로 가질 않았는데 이것은 그동안 포스트맨의 요청을 보낼때 Text 형식으로 보냈기 때문이었다. 웹페이지에서 서버로 데이터를 보낼때 JSON 형식으로 보내기 때문에, @RequestBody를 달아줘서 JSON 형태로 온 데이터를 객에체 매핑 시켜줬는데, 포스트맨에서는 Text 형식으로 보내니깐 JSON 형태가 아니라서 요청을 받지 못했던 것이다. 따라서 포스트맨에서 테스트를 해보려면 raw 에서 JSON을 선택해서 JSON 형태의 데이터로 작성해서 보내야 실제로 웹페이지에서 보내는 방식과 동일하게 테스트 구현이 가능하다.
이 주문 기능을 동작하면서 느낀게 있다, 알고나니 정말 너무나도 허무했던 내용이지만, 애초에 검색을 어떻게 해야될까? 라는 것 조차 제대로 몰랐던 것이다. 처음에는 찾아보려고 별 생각을 다해가며 고쳐봤는데, 아무리 해도 답이 보이지 않았다. 결국 검색을 하지 않고서는 절대 해결할 수 없는 문제였는데, 그 검색을 위에서 내가 검색한 문장으로 치는것을 생각하기까지도 오랜 시간이 걸려서 금방 해결을 못했다고 생각한다.
오류가 나서 해결을 해보려 했는데도 도저히 모를때, 어떤 문제가 발생하였는지 명확하게 파악하여, 해당 문제를 해결하기 위해 자료를 찾아보는 것 또한 실력이라고 생각하기에, 오늘 새벽까지 해보며 머리 싸맨 덕분에 한층 더 성장해진 내가 되었다고 생각한다.
오늘의 느낀점 👀
오늘은 로그인 기능 구현 전 컨트롤러, 서비스, 레포지토리 클래스 작성하는 것을 다시한번 실습해보는 시간을 가졌다. 처음엔 어렵기만 했던 것들이 계속 하다보니 눈에 조금씩 보이기 시작하는 것 같다.
내일 로그인 기능을 드디어 배우는데, 벌써부터 재밌을 것 같다. 공부를 하면서 느끼는것은 내가 그동안 배움의 즐거움을 느껴본적이 있던가 라는 생각이 든다. 이번 부트캠프를 통해 뒤늦게나마 그런 감정을 느끼고 있고, 그덕에 더 열심히 하려고 하는 것 같다.
앞으로 배울 많은 내용들을 빨리 접해보고 싶은 마음이다.😎