1. 문제 상황

- 결과 내용이 반복적으로 나옴

2. 원인

- 객체 간 순환 참조(circular reference)가 원인일 수 있음

- Product 객체가 Store 객체를 참조하고, Store 객체도 다시 Product 객체를 참조

3. 해결 방법

1) DTO 사용하기: 엔티티를 직접 반환하는 대신 Data Transfer Object (DTO)를 사용하여 API 응답을 구성

- DTO 클래스 정의

package com.zerobase.ecproject.dto;

import lombok.Data;

@Data
public class ProductDTO {
  private Long id;
  private String name;
  private String description;
  private Integer price;
  private String category;
  private Integer stockQuantity;
  private Long storeId;
}

- 엔티티를 DTO로 변환하는 메서드 구현 (ProductService)

  public ProductDTO convertToDTO(Product product) {
    ProductDTO dto = new ProductDTO();
    dto.setId(product.getId());
    dto.setName(product.getName());
    dto.setDescription(product.getDescription());
    dto.setPrice(product.getPrice());
    dto.setCategory(product.getCategory());
    dto.setStockQuantity(product.getStockQuantity());
    dto.setStoreId(product.getStore() != null ? product.getStore().getId() : null);
    return dto;
  }

- ProductController 수정, ProductDTO 객체를 Product 엔티티 객체로 변환, Product 엔티티를 ProductDTO 객체로 변환

@PostMapping("/{storeId}")
  public ResponseEntity<ProductDTO> createProduct(@RequestBody ProductDTO productDTO,
      @PathVariable Long storeId) {
    Product product = convertToEntity(productDTO);
    Product createdProduct = productService.createProduct(product, storeId);
    return ResponseEntity.ok(convertToDTO(createdProduct));
  }

  private Product convertToEntity(ProductDTO dto) {
    Product product = new Product();
    product.setName(dto.getName());
    product.setDescription(dto.getDescription());
    product.setPrice(dto.getPrice());
    product.setStockQuantity(dto.getStockQuantity());
    return product;
  }

  private ProductDTO convertToDTO(Product product) {
    ProductDTO dto = new ProductDTO();
    dto.setId(product.getId());
    dto.setName(product.getName());
    dto.setDescription(product.getDescription());
    dto.setPrice(product.getPrice());
    dto.setCategory(product.getCategory());
    dto.setStockQuantity(product.getStockQuantity());
    dto.setStoreId(product.getStore() != null ? product.getStore().getId() : null);
    return dto;
  }

- 문제 해결


2) Jackson 라이브러리 설정

- @JsonManagedReference와 @JsonBackReference 애노테이션을 사용하여 순환 참조를 관리

- @JsonIgnore를 사용하여 직렬화에서 특정 필드를 제외

3) Lombok의 @ToString.Exclude 사용

- @ToString 메서드에서 순환 참조가 발생하는 필드를 제외

- toString() 메서드 호출 시 순환 참조로 인한 문제를 방지

<로그인/로그아웃 기능 구현>

1) UserDetailsService 인터페이스

- 데이터베이스 사용자 정보를 불러오는 인터페이스

- loadUserByUsername() 메서드를 가지고 있으며 UserDetails 반환

@Service
@Transactional
@RequiredArgsConstructor
public class MemberService implements UserDetailsService {

    private final MemberRepository memberRepository;
    // 의존성 주입, 회원 정보에 대한 CRUD 연산을 데이터베이스에 위임

    public Member saveMember(Member member) {   // 새 회원 저장 메서드
        validateDuplicateMember(member);        // 먼저 중복 검사 실행
        return memberRepository.save(member);
    }

    private void validateDuplicateMember(Member member) {
        Member findMember = memberRepository.findByEmail(member.getEmail());
        if (findMember != null) {
            throw new IllegalStateException("이미 가입된 회원입니다.");
        }
    }

    @Override       // UserDetailsService 인터페이스에서 선언된 loadUserByUsername 메서드 오버라이드
    public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException{
        // 이메일을 기반으로 회원 정보 조회
        Member member = memberRepository.findByEmail(email);

        // 사용자 존재 여부 검사
        if(member == null){
            throw new UsernameNotFoundException(email);
        }

        // UserDetails 생성 및 반환
        return User.builder()
                .username(member.getEmail())
                .password(member.getPassword())
                .roles(member.getRole().toString())
                .build();
    }
}

2) SecurityConfig 시큐리티 설정

- 기존 Spring Security 중 deprecated 된 것이 많음, 람다식으로 사용 권장

@Configuration      // 클래스가 Spring IoC 컨테이너에 의해 Bean 정의의 소스로 사용됨을 나타냄
@EnableWebSecurity  // Spring Security 설정을 활성화
public class SecurityConfig {

     @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .csrf(csrf -> csrf.disable())           // CSRF(크로스 사이트 요청 위조) 보호 기능을 비활성화
                .authorizeRequests(auth -> auth         // 요청에 대한 접근 제어 설정
                        .requestMatchers("/members/login", "/members/new").permitAll()       // 회원 가입 페이지는 인증되지 않은 사용자도 접근 가능
                        .anyRequest().authenticated()   // 그 외의 모든 요청은 인증된 사용자만 접근 가능
                )
                .formLogin(form -> form                 // 폼 로그인 방식 설정
                        .loginPage("/members/login")     // 사용자 정의 로그인 페이지의 URL을 지정
                        .defaultSuccessUrl("/")         // 로그인 성공 시 리다이렉트될 기본 URL
                        .usernameParameter("email")     // 로그인 폼에서 사용자 이름 대신 이메일 사용
                        .failureUrl("/members/login/error")     // 로그인 실패 시 리다이렉트될 URL
                )
                .logout(logout -> logout                // 로그아웃 설정 정의
                        .logoutRequestMatcher(new AntPathRequestMatcher("/members/logout")) // 로그아웃 URL
                        .logoutSuccessUrl("/")          // 로그아웃 성공 시 리다이렉트될 URL
                );
        return http.build();                            // HttpSecurity 구성을 빌드하여 SecurityFilterChain 객체를 반환

    }

    @Bean
    // 비밀번호를 안전하게 저장하고 검증하는 메커니즘을 제공하는 인터페이스
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(); // BCrypt 해싱 함수를 사용하여 비밀번호를 인코딩
    }
}

 

<페이지 권한 설정>

1) 상품 등록 페이지에 접근하는 컨트롤러 ItemController 구현

package com.ecproject.onlinestore.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class ItemController {
  
  @GetMapping("admin/item/new")   //관리자 접근만 가능
  public String itemForm() {
    return "/item/itemForm";
  }
}

2) CustomAuthenticationEntryPoint 클래스: AuthenticationEntryPoint 인터페이스 구현

- Spring Security에서 인증되지 않은 사용자가 보호된 리소스에 접근하려고 할 때 실행되는 로직 정의

- commence 메서드: 사용자에게 HTTP 401 Unauthorized 에러와 "Unauthorized"라는 메시지를 전달하도록 구현

package com.ecproject.onlinestore.config;

// import문 생략

public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {

  @Override
  public void commence(HttpServletRequest request, HttpServletResponse response,
      AuthenticationException authException) throws IOException,
      ServletException {
    response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
  }
}

3) SecurityConfig 보완

  @Bean
  public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .csrf(csrf -> csrf.disable())           // CSRF(크로스 사이트 요청 위조) 보호 기능을 비활성화
        .authorizeRequests(auth -> auth         // 요청에 대한 접근 제어 설정
            .requestMatchers("/members/login", "/members/new")
            .permitAll()       // 회원 가입 페이지는 인증되지 않은 사용자도 접근 가능
            .requestMatchers("/admin/**")
            .hasRole("ADMIN")  // 관리자 페이지는 'ROLE_ADMIN' 권한을 가진 사용자만 접근 가능
            .anyRequest().authenticated()   // 그 외의 모든 요청은 인증된 사용자만 접근 가능
        )
        .exceptionHandling(e -> e
            .authenticationEntryPoint(new CustomAuthenticationEntryPoint())
        )	// 인증되지 않은 사용자가 보호된 리소스에 접근을 시도할 때
          // 'CustomAuthenticationEntryPoint' 내의 'commence' 메서드가 실행됨
        .formLogin(form -> form                 // 폼 로그인 방식 설정
            .loginPage("/members/login")     // 사용자 정의 로그인 페이지의 URL을 지정
            .defaultSuccessUrl("/")         // 로그인 성공 시 리다이렉트될 기본 URL
            .usernameParameter("email")     // 로그인 폼에서 사용자 이름 대신 이메일 사용
            .failureUrl("/members/login/error")     // 로그인 실패 시 리다이렉트될 URL
        )
        .logout(logout -> logout                // 로그아웃 설정 정의
            .logoutRequestMatcher(new AntPathRequestMatcher("/members/logout")) // 로그아웃 URL
            .logoutSuccessUrl("/")          // 로그아웃 성공 시 리다이렉트될 URL
        );
    return http.build();                            // HttpSecurity 구성을 빌드하여 SecurityFilterChain 객체를 반환
  }

 

<Product(상품) 설계>

1) Entity 생성

package com.ecproject.onlinestore.entity;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.*;

import java.time.LocalDateTime;

@Entity
@Getter
@Setter
@NoArgsConstructor
@ToString
public class Product {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;
    private String details;
    private int price;
    private int quantity;

    private LocalDateTime releaseTime;
    private LocalDateTime updateTime;
}

2) MySql 데이터베이스 테이블 생성

3) MySql 관련 의존성 추가

// build.gradle
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'com.mysql:mysql-connector-j'
// application.properties
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/(데이터베이스명)
spring.datasource.username=(아이디)
spring.datasource.password=(비밀번호)
spring.jpa.show-sql = true
spring.jpa.database = mysql

4) ProductRepository 생성

package com.ecproject.onlinestore.repository;

import com.ecproject.onlinestore.entity.Product;
import org.springframework.data.jpa.repository.JpaRepository;

public interface ProductRepository extends JpaRepository<Product, Long> {
}

 

<회원가입 기능>

1) Spring Security 의존성 추가

// build.gradle
implementation 'org.springframework.boot:spring-boot-starter-security'

2) SecurityConfig 작성

- Spring Security의 기본 설정을 커스터마이징하여 애플리케이션의 보안 요구 사항에 맞게 조정

- 'WebSecurityConfigurerAdapter' 클래스를 상속받아 구현하는 방식은 더이상 쓰이지 않음

- 'SecurityFilterChain' 사용이 권장됨

package com.ecproject.onlinestore.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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 static org.springframework.security.config.Customizer.withDefaults;

@Configuration      // 클래스가 Spring IoC 컨테이너에 의해 Bean 정의의 소스로 사용됨을 나타냄
@EnableWebSecurity  // Spring Security 설정을 활성화
public class SecurityConfig {

    @Bean
    // HttpSecurity 객체를 사용하여 HTTP 요청에 대한 보안 구성을 정의
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.authorizeRequests(authz -> authz
            .anyRequest().authenticated())  // 모든 요청에 대해 인증을 요구함
            .formLogin(withDefaults());  // 기본 로그인 폼을 제공함
        return http.build();
    }

    @Bean
    // 비밀번호를 안전하게 저장하고 검증하는 메커니즘을 제공하는 인터페이스
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(); // BCrypt 해싱 함수를 사용하여 비밀번호를 인코딩
    }
}

 

3) MemberRole enum 생성

- 각각의 멤버가 일반 유저인지 관리자인지 구분

package com.ecproject.onlinestore.entity;

public enum MemberRole {
    USER, ADMIN     // 각각의 멤버가 일반 유저인지 관리자인지 구분
}

4) DTO 생성

- DTO(Data Transfer Object): 계층 간 데이터 전송을 위해 사용되는 객체

- 클라이언트와 서버 간, 또는 서비스 계층과 데이터 액세스 계층 간의 데이터 교환을 위해 사용

- 'MemberFormDto': 사용자가 회원가입 양식에 입력하는 데이터를 담는 DTO

package com.ecproject.onlinestore.dto;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class MemberFormDto {

    private String name;
    private String email;
    private String password;
    private String address;
}

5) Member Entity 생성

- @Enumerated(EnumType.STRING): 열거형 값을 문자열로 데이터베이스에 저장, 열거형의 순서가 변경되더라도 데이터베이스에 영향 X

- createMember 메서드: MemberFormDto 객체와 비밀번호를 암호화하기 위한 PasswordEncoder를 받아 새로운 Member 인스턴스를 생성하고 반환

package com.ecproject.onlinestore.entity;

import com.ecproject.onlinestore.dto.MemberFormDto;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

import org.springframework.security.crypto.password.PasswordEncoder;


@Entity
@Getter
@Setter
@ToString
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @Column(unique = true)      // 회원은 이메일을 통해 유일하게 구분
    private String email;

    private String password;
    private String address;

    @Enumerated(EnumType.STRING)    
    private Role role;

    public static Member createMember(MemberFormDto memberFormDto,
                                      PasswordEncoder passwordEncoder) {
        Member member = new Member();
        member.setName(memberFormDto.getName());
        member.setEmail(memberFormDto.getEmail());
        member.setAddress(memberFormDto.getAddress());
        String password = passwordEncoder.encode(memberFormDto.getPassword());
        member.setPassword(password);
        member.setRole(Role.ADMIN);
        return member;
    }

}

6) MemberRepository 생성

package com.ecproject.onlinestore.repository;

import com.ecproject.onlinestore.entity.Member;
import org.springframework.data.jpa.repository.JpaRepository;

public interface MemberRepository extends JpaRepository<Member, Long> {
    Member findByEmail(String email);   //회원 가입시 중복된 회원이 있는지 이메일로 검사
}

7) MemberService 생성

package com.ecproject.onlinestore.service;

import com.ecproject.onlinestore.entity.Member;
import com.ecproject.onlinestore.repository.MemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional
@RequiredArgsConstructor
public class MemberService {

    private final MemberRepository memberRepository;
    // 의존성 주입, 회원 정보에 대한 CRUD 연산을 데이터베이스에 위임

    public Member saveMember(Member member) {   // 새 회원 저장 메서드
        validateDuplicateMember(member);        // 먼저 중복 검사 실행
        return memberRepository.save(member);
    }

    private void validateDuplicateMember(Member member) {
        Member findMember = memberRepository.findByEmail(member.getEmail());
        if (findMember != null) {
            throw new IllegalStateException("이미 가입된 회원입니다.");
        }
    }
}

8) MemberController 생성

- 회원가입이 성공하면 메인 페이지로 리다이렉트

package com.ecproject.onlinestore.controller;

import com.ecproject.onlinestore.dto.MemberFormDto;
import com.ecproject.onlinestore.entity.Member;
import com.ecproject.onlinestore.service.MemberService;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/members")
@RequiredArgsConstructor
public class MemberController {

    // 의존성 주입
    private final MemberService memberService;
    private final PasswordEncoder passwordEncoder;

    @GetMapping("/new")
    public String memberForm(Model model){                      // 회원 가입 양식 요청 메서드
        model.addAttribute("memberFormDto", new MemberFormDto());
        return "member/memberForm";
    }
    @PostMapping("/new")
    public String memberForm(MemberFormDto memberFormDto){      // 회원 가입 양식 제출 메서드
        Member member = Member.createMember(memberFormDto, passwordEncoder);
        memberService.saveMember(member);

        return "redirect:/";
    }
}

9) MainController 생성

- 애플리케이션의 홈페이지 또는 메인 페이지를 사용자에게 보여주는 역할을 수행

- 사용자가 웹 브라우저에서 루트 URL을 방문하면, "main" 뷰가 렌더링되어 사용자에게 응답으로 전송

package com.ecproject.onlinestore.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class MainController {
    @GetMapping("/")
    public String main() {
        return "main";
    }
}

10) MemberFormDto 유효성 검증 어노테이션 추가

package com.ecproject.onlinestore.dto;

import lombok.Getter;
import lombok.Setter;
import org.hibernate.validator.constraints.Length;

import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;

@Getter
@Setter
public class MemberFormDto {

    @NotBlank(message = "이름은 필수 입력 값입니다.")
    private String name;

    @NotBlank(message = "이메일은 필수 입력 값입니다.")
    @Email(message = "이메일 형식으로 입력해주세요.")
    private String email;

    @NotBlank(message = "비밀번호는 필수 입력 값입니다.")
    @Length(min=4, max=10, message = "비밀번호는 4자 이상 10자 이하로 입력해주세요.")
    private String password;

    @NotEmpty(message = "주소는 필수 입력값입니다.")
    private String address;
}

11) MemberController 기능 추가

회원 가입이 성공하면 메인 페이지로 리다이렉트

- 실패하면 다시 회원 가입 페이지로 돌아가 실패 이유 출력

- @Valid: MemberFormDto 객체에 설정된 검증 조건을 자동으로 검사

- BindingResult: 검증 과정에서 발생한 에러 정보를 담음

@Controller
@RequestMapping("/members")
@RequiredArgsConstructor
public class MemberController {

    // 의존성 주입
    private final MemberService memberService;
    private final PasswordEncoder passwordEncoder;

    @GetMapping("/new")         // 회원 가입 양식 요청 메서드
    public String memberForm(Model model){
        model.addAttribute("memberFormDto", new MemberFormDto());
        return "member/memberForm";
    }
    @PostMapping("/new")        // 회원 가입 양식 제출 메서드
    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:/";
        // 성공적으로 회원 가입이 완료되면 루트 경로(/)로 리다이렉트
    }
}

12) thymeleaf 설정, memberForm.html 작성

implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>회원가입</title>
</head>
<body>
<h1>회원가입</h1>
<form th:action="@{/members/new}" th:object="${memberFormDto}" method="post">
    <p>이름: <input type="text" th:field="*{name}" /></p>
    <p>이메일: <input type="email" th:field="*{email}" /></p>
    <p>비밀번호: <input type="password" th:field="*{password}" /></p>
    <p>주소: <input type="text" th:field="*{address}" /></p>
    <p><button type="submit">가입하기</button></p>
</form>
</body>
</html>

http://localhost:8080/members/new

 

13) SecurityConfig 수정

회원 가입이 성공하면 메인 페이지로 리다이렉트

@Configuration      // 클래스가 Spring IoC 컨테이너에 의해 Bean 정의의 소스로 사용됨을 나타냄
@EnableWebSecurity  // Spring Security 설정을 활성화
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .csrf(csrf -> csrf.disable()) // CSRF 보호 비활성화
                .authorizeRequests(authz -> authz
                        .requestMatchers("/members/new").permitAll() // '/members/new' 경로에 인증되지 않은 접근 허용
                        .anyRequest().authenticated() // 그 외 모든 요청에 대해 인증 요구
                )
                .formLogin(withDefaults()); // 기본 로그인 폼 사용

        return http.build();
    }

    @Bean
    // 비밀번호를 안전하게 저장하고 검증하는 메커니즘을 제공하는 인터페이스
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(); // BCrypt 해싱 함수를 사용하여 비밀번호를 인코딩
    }
}

 

1. 프로젝트 생성

- Spring Boot DevTools: 개발 중에 자동 재시작과 라이브 리로드 등의 기능을 제공

- Lombok: 자바에서 반복되는 코드(예: getter, setter, toString 등)를 어노테이션을 통해 자동으로 생성

- Spring Web: 웹 애플리케이션을 만들기 위해 필요, RESTful 애플리케이션을 만드는 데 사용

- Spring Data JPA: 데이터베이스와의 상호작용을 추상화하고 간소화하는 데 사용, JPA를 사용하여 ORM(Object-Relational Mapping)을 구현

2. 간단한 ERD 그리기

- 셀러(판매자)는 상품을 업로드 가능

- 커스터머(구매자)는 상품을 카트에 담거나 주문 가능

*Lucidchart 사용

3. 프로젝트 구조

1) Controller

- 클라이언트의 HTTP 요청 처리 & 응답 반환, CRUD 연산 매핑 매핑

- 사용자의 입력을 받아 서비스 계층으로 전달하고, 서비스 계층으로부터 받은 데이터를 클라이언트에게 반환

2) Entity

- 구매자, 판매자, 상품, 장바구니 등 핵심 비즈니스 엔티티

- 데이터베이스의 테이블과 매핑되는 도메인 모델 정의

3) Service

- 비즈니스 로직 메서드 제공

- 컨트롤러와 리포지토리 계층 사이에서 데이터를 처리하고 비즈니스 규칙 적용

4) Repository

- 데이터베이스와 커뮤니케이션 담당

- Spring Data JPA를 사용하여 CRUD 연산 메서드 자동 생성

5) Config

- 애플리케이션의 구성(configuration) 관리

6) Exception

- 발생 가능한 예외 처리 관리

- 사용자 정의 예외, 글로벌 예외 핸들러 등

1. userapi 하위 모듈 생성

 

2. 'SendMailForm' 클래스

  • 이메일 전송 요청에 필요한 데이터를 모델링
package com.example.userapi.client.mailgun;

import lombok.*;

@NoArgsConstructor
@AllArgsConstructor
@Getter
@Builder
@Data
public class SendMailForm {
    private String from;
    private String to;
    private String subject;
    private String text;
}​

 

3. 'MailClient' 인터페이스

package com.example.userapi.client;

import com.example.userapi.client.mailgun.SendMailForm;
import feign.Response;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.cloud.openfeign.SpringQueryMap;
import org.springframework.web.bind.annotation.PostMapping;

@FeignClient(name="mailgun",url="https://api.mailgun.net/v3/")
@Qualifier("mailgun")
public interface MailgunClient {

    @PostMapping("(메일건 API 키).mailgun.org/messages")
    Response sendEmail(@SpringQueryMap SendMailForm form);
}
  • Mailgun의 이메일 전송 API를 호출하기 위한 Feign 클라이언트 정의
  • Feign: 선언적  서비스 클라이언트, HTTP 요청을 손쉽게 보낼  있게 해줌
  • @FeignClient(name="mailgun", url="https://api.mailgun.net/v3/"): 이 클라이언트의 이름을 "mailgun"으로 지정하고, Mailgun API의 기본 URL을 설정
  • @Qualifier("mailgun"): 이 클라이언트의 빈(bean)에 "mailgun"이라는 구체적인 식별자를 부여
  • sendEmail 메소드: @PostMapping 어노테이션을 사용하여 Mailgun의 이메일 전송 API 엔드포인트를 지정. SendMailForm 객체를 받아 Mailgun API에 전송하고, Response 객체로 결과를 반환.

4. Mailgun basic code (참고)

import java.io.File;
import com.mashape.unirest.http.HttpResponse;
import com.mashape.unirest.http.JsonNode;
import com.mashape.unirest.http.Unirest;
import com.mashape.unirest.http.exceptions.UnirestException;
public class MGSample {
	 // ...
	public static JsonNode sendSimpleMessage() throws UnirestException {
		HttpResponse<JsonNode> request = Unirest.post("https://api.mailgun.net/v3/" + YOUR_DOMAIN_NAME + "/messages"),
			.basicAuth("api", API_KEY)
			.queryString("from", "Excited User <USER@YOURDOMAIN.COM>")
			.queryString("to", "artemis@example.com")
			.queryString("subject", "hello")
			.queryString("text", "testing")
			.asJson();
		return request.getBody();
	}
}

 

* 이 구성을 통해, Spring 애플리케이션은 MailgunClient 인터페이스를 사용하여 Mailgun API에 이메일 전송 요청을 할 수 있으며, SendMailForm 객체를 사용하여 필요한 요청 데이터를 쉽게 전달할 수 있습니다.

 

5. 'EmailSendService' 클래스

package com.example.userapi.client.service;

import com.example.userapi.client.MailgunClient;
import com.example.userapi.client.mailgun.SendMailForm;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class EmailSendService {
    private final MailgunClient mailgunClient;

    public void sendEmail() {
        SendMailForm form = SendMailForm.builder()
                .from("zerobase-test.my.com")
                .to("zerobase-test.my.com")
                .subject("Test email from zero base")
                .text("my text")
                .build();
        mailgunClient.sendEmail(form);
    }
}

6. sendEmail 테스트

package com.example.userapi.client.service;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class EmailSendServiceTest {
    @Autowired
    private EmailSendService emailSendService;

    @Test
    public void EmailTest(){
        emailSendService.sendEmail();
    }
}

새 스키마 만들기:

  • Schemas 탭 내의 빈 공간에서 마우스 오른쪽 버튼을 클릭하거나 Navigator 패널 상단의 툴바에서 "Create Schema" 옵션(데이터베이스 아이콘에 더하기 기호가 있는 것)을 찾습니다.
  • "Create Schema"를 클릭하면 "Create Schema" 대화 상자가 열립니다.

 

새 스키마 구성:

  • Schema Name: 새 스키마의 이름을 입력.
  • Default Collation: 스키마의 기본 정렬 규칙을 선택합니다. 정렬 규칙 설정은 데이터베이스 내 데이터가 어떻게 정렬되고 비교되는지 결정합니다. 대부분의 애플리케이션에는 기본값이 적합하지만, 특정 언어나 특별한 경우에는 특정 정렬 규칙이 필요할 수 있습니다.
  • "Apply" 버튼을 클릭하여 스키마를 생성.
  1.  

#참고도서: '스프링 프레임워크 첫걸음' (키노시타 마사아키, 위키북스)

 

09장: 애플리케이션 만들기

 

1. OX 퀴즈

1) 기능 목록

- 등록 기능, 갱신 기능, 삭제 기능, 목록 표시 기능, 게임 기능

2) URL 목록

- GET, /quiz: 퀴즈 목록을 표시

- POST, /quiz/insert: 등록 처리를 실행

- GET, /quiz/{id}: 갱신 화면을 표시

- POST, /quiz/update: 갱신 처리를 실행

- POST, /quiz/delete: 삭제 처리를 실행

- GET, /quiz/play: 퀴즈 화면을 표시

- POST, /quiz/check: 퀴즈 답을 체크

 

2. 레이어

1) 애플리케이션 레이어

- 클라이언트에서 받은 요청을 제어하고 도메인 레이어를 사용하여 애플리케이션을 제어.

- Controller: 요청을 처리에 매핑하고 결과를 뷰에 넘겨주는 제어를 함. 주요 처리는 Controller 안에서 실행하지 않고 '도메인 레이어'의 Service를 호출.

- Form: 화면의 폼을 표현. 입력한 값을 Controller에 넘겨주며, Controller에서 화면에 결과를 출력할 때도 사용. 도메인 레이어가 애플리케이션 레이어에 의존하지 않도록 Form에서 도메인 객체로 변환하거나 도메인 객체에서 Form으로 변환하는 것을 애플리케이션 레이어에서 수행.

- View: 화면 표시를 담당.

2) 도메인 레이어

- 도메인 객체에 대해 애플리케이션의 서비스 처리를 실행.

- Entity: 서비스 처리를 실행할 때 필요한 자원.

- Service: 애플리케이션의 서비스 처리를 담당.

- Repository: 인터페이스. 데이터베이스의 데이터 조작 내용만 정의(구현 내용은 작성하지 않음).

3) 인스라스트럭처 레이어

- 도메인 객체에 대해 CRUD 조작을 해서 데이터의 영속화를 담당.

- Repositorylmpl: 도메인 레이어에서 정의한 Repository의 구현 클래스.

- O/R Mapper: O(Object; 객체)와 R(Relational; 관계형 데이터베이스) 간의 데이터를 매핑

 

3. 데이터베이스/프로젝트 생성

1) dependencies

- Spring Boot DevTools: 스프링 부트 개발 툴. 자동 재실행 등 개발에 편리한 기능 포함.

- Lombok: 어노테이션을 부여하는 것으로 getter와 setter 등을 코드로 작성하지 않아도 자동으로 구현해줌.

- Spring Data JDBC: 스프링 데이터에서 제공하는 OR Mapper.

- Validation: 유효성 검사 기능인 'Bean Validation'과 'Hibernate Validator'를 사용할 수 있게 함.

- Thymeleaf: 스프링 부트에서 추천하는 템플릿 엔진

- Spring Web: 스프링 MVC

테이블 생성

 

#참고도서: '스프링 프레임워크 첫걸음' (키노시타 마사아키, 위키북스)

 

08장: 유효성 검사 기능 알아보기

1. 유효성 검사

- 입력 내용이 요건에 만족하는지 그 타당성을 확인

- 단일 항목 검사/상관 항목 검사

1) 단일 항목 검사

- 입력 항목 하나에 대해 설정하는 입력 체크 기능

- @NotNull, @Max, @Size, @AssertTrue, @Past, @Valid, @Length, @Email, @URL 등

2) 커스텀 유효성 검사

- 여러 필드에 대해 혼합해서 체크하는 것

- 상관 항목(서로 관련이 있는 항목) 검사

- Bean Validation 사용, 스프링 프레임워크 Validator 인터페이스 구현

 

2. 단일 항목 검사 프로그램 만들기

- HTML의 <form> 태그에 바인딩되는 Form 클래스 인스턴스를 'form-backing bean'이라고 부르고 @ModelAttribute 어노테이션을 사용해 연결

- @Validated 어노테이션을 부여한 클래스와 BindingResult 인터페이스를 함께 인수로 사용하고 반드시 @Validated -> BindingResult 순으로 사용

- messages.properties는 '키=값'의 형태로 프로퍼티 정의

 

3. 커스텀 유효성 검사기를 사용하는 프로그램 만들기

- 스프링 프레임워크가 제공하는 Validator 인터페이스를 구현하는 커스텀 유효성 검사기 생성

- 컨트롤러에 앞에서 만든 커스텀 유효성 검사기를 주입(인젝션)하고 WebDataBinder 인터페이스의 addValidators 메서드로 커스텀 유효성 검사기를 등록하여 스프링 MVC에서 이용할 수 있게 하기

 

+ Recent posts