개발환경

 

  1. 운영체제 : Mac OS
  2. 통합개발환경(IDE) : IntelliJ Ultimate
  3. JDK version : JDK 22
  4. Spring Boot version : 3.3.2
  5. Database : MySQL
  6. Build Tool : Maven
  7. Packaging : Jar

 

Dependencies
  • Spring Web
  • Spring Data JPA
  • MySQL Driver
  • H2 Database
  • Thymeleaf
  • Lombok
  • Spring Security
  • Spring Boot DevTools
  • querydsl
  • thymeleaf-layout-dialect
  • validation
  • thymeleaf-extras-springsecurity6

 

패키지 설계

 

스프링 시큐리티 설정


  • config 패키지에 securityconfig 클래스 생성
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    
        http.authorizeHttpRequests(configurer ->
                configurer
                        .requestMatchers("/h2-console/**").permitAll() // H2 콘솔에 대한 접근 허용
                        .anyRequest().authenticated() // 나머지 요청에 대해 인증 필요

        );
        
        // use HTTP Basic authentication
        http.httpBasic(Customizer.withDefaults());

        // disable Cross Site Request Forgery(CSRF)
        http.csrf(csrf -> csrf.disable());

        return http.build();
    
    }
    
    // H2 콘솔 관련 설정
    @Bean
    @ConditionalOnProperty(name = "spring.h2.console.enabled", havingValue = "true")
    public WebSecurityCustomizer configureH2ConsoleEnable() {
        return web -> web.ignoring()
                .requestMatchers(PathRequest.toH2Console());
    }

 

  • @Configuration : Spring의 구성 클래스를 나타내며, Bean 정의를 포함하는 클래스를 의미
  • @EnableWebSecurity : Spring Security를 활성화. Spring Security의 설정을 정의하고 적용
💡  BCryptPasswordEncoder
      비밀번호를 암호화하고 검증하는 데 사용되는 PasswordEncoder 구현체. BCrypt 알고리즘을 사용하여 비밀번호를 암호화한다. BCrypt는 비밀번호 해시를 생성하는 데 안전한 방법으로 널리 사용

 

 

SecurityFilterChain HTTP 요청에 대한 보안 필터 체인을 정의. HttpSecurity 객체를 사용하여 필터 체인을 설정.

HttpSecurity : HTTP 요청에 대한 보안 설정을 구성하는 데 사용됨

authorizeHttpRequests(configurer -> ...) : HTTP 요청에 대한 접근 권한을 설정

requestMatchers(...) : URL 패턴에 따라 요청을 필터링.

permitAll() : 해당 URL 패턴에 대해 인증 없이 접근을 허용.

anyRequest().authenticated() : 위에서 정의된 패턴을 제외한 모든 요청은 인증된 사용자만 접근할 수 있도록 설정.

httpBasic(Customizer.withDefaults()) : HTTP Basic Authentication을 활성화. 이 방법은 간단한 인증 방법으로 사용자 이름과 비밀번호를 HTTP 헤더에 포함시켜 인증을 수행.

csrf.disable() : Cross Site Request Forgery (CSRF) 보호를 비활성화. CSRF는 웹 애플리케이션에서 악의적인 요청을 방지하기 위한 보안 메커니즘. 일반적으로 비활성화하지 않는 것이 좋지만, API 또는 비상 상황에서는 비활성화할 수 있다.

 

https://wikidocs.net/162150

 

3-05 스프링 시큐리티란?

* `[완성 소스]` : [https://github.com/pahkey/sbb3/tree/v3.05](https://github.com/pahkey/sbb3/tree/v3.05…

wikidocs.net

 

📍 내가 노션에 정리한 내용!

https://www.notion.so/5-Spring-Boot-Security-b95a14467d6f4a87937a66b8d217c2d2?pvs=4

https://www.notion.so/8-Spring-MVC-Security-3fc33d56e52e4d809dac4b7fbdc0f025?pvs=4

 

 

.requestMatchers("/**").permitAll() - 일단 스프링 시큐리티 적용안하고 바로 화면 보이도록 설정...

 

 

엔티티, DTO 코드 설정


 

1. 일반 유저 / 관리자 역할 코드

  •  com.doraflower 패키지 아래 'constant' 키지 생성 - Role.java 생성
  •  enum(열거형)

 

https://velog.io/@mooh2jj/Java-Enum%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94-%EC%9D%B4%EC%9C%A0

 

[Java] Enum을 사용하는 이유와 활용법

Java enum은 제한된 값 목록을 갖는 타입입니다. enum은 다음과 같은 이점을 갖습니다.enum은 컴파일 타임에 타입 안정성을 보장합니다. 특정 범위의 값만 사용 가능하므로 컴파일 오류나 런타임 예

velog.io

 

public enum Role {
    USER, ADMIN
}

 

 

2. MemberFormDTO 클래스 코드 - 회원가입 화면으로부터 넘어오는 정보를 담는다.

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class MemberFormDTO {

    @NotBlank(message = "Name is required")
    private String name;

    @NotBlank(message = "Email is required")
    @Pattern(regexp = "^[\\w-.]+@([\\w-]+\\.)+[\\w-]{2,4}$",
            message = "Please enter a valid email address.")
    private String email;

    @NotBlank(message = "Password is required")
    @Length(min=8, max=16, message = "Enter a password between 8 and 16 characters, including numbers.")
    private String password;

    @NotBlank(message = "Address is required")
    private String address;
}

 

2024.07.25 - [Spring & Spring Boot] - @Lombok 및 Entity 관련 어노테이션

 

  • 회원가입 폼에서 받을 내용 - 이름 | 이메일 주소 (로그인 시 확인) | 비밀번호 | 주소
  • message는 유효성 검사시 에러가 발생하면 출력될 내용!

 

3. Member 클래스 코드 - 회원 정보를 저장하는 엔티티 (DB에서 관리)

@Entity
@Table(name = "member")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Member {

    @Id
    @GeneratedValue (strategy = GenerationType.IDENTITY)
    @Column(name="member_id")
    private Long memberId;

    @Column(nullable = false)
    private String name;

    @Column(unique = true, nullable = false)
    private String email;

    @Column(nullable = false)
    private String password;

    @Column(nullable = false)
    private String address;

    @Enumerated(EnumType.STRING)
    private Role role;

    public static Member createMember(MemberFormDTO memberFormDTO,
                                      PasswordEncoder passwordEncoder) {

        String password = passwordEncoder.encode(memberFormDTO.getPassword());

        Member member = Member.builder()
                .name(memberFormDTO.getName())
                .email(memberFormDTO.getEmail())
                .password(password)
                .address(memberFormDTO.getAddress())
                .role(Role.USER)
                .build();

        return member;
    }

}

 

  • 데이터베이스 테이블 이름은 'member'로 지정
  • 이메일을 통해 회원 구분을 위해 'unique'설정
  • Role은 string으로 저장하도록 설정
  • Member 엔티티 생성하는 메소드 설정 - 파라미터로 dto와 passwordencoder를 받음
  • 패스워드는 db에 암호화 돼서 저장됨
  • role은 'USER'로 설정해줌 -> ADMIN 먼저 만들고 바꿔줘도 됨

 

 

Repository 코드 설정 - DAO 역할


  • repository 패키지 아래 MemberRepository 인터페이스 생성
public interface MemberRepository extends JpaRepository<Member, Long> {

    Member findByEmail(String email);
}
  • 회원가입 시 중복된 회원이 있는지 검사하기 위해 이메일로 회원을 검사할 수 있도록 쿼리 메소드 작성

 

 

Service 코드 설정


  • service 패키지 아래 MemberService 인터페이스, MemberServiceImpl 클래스 생성
특성 서비스 계층이 필요한 경우 서비스 계층이 필요없는 경우
비즈니스 로직의 복잡성 복잡한 비즈니스 로직이 있는 경우 비즈니스 로직이 거의 없거나 단순한 경우
유지보수 비즈니스 로직을 중앙화하여 유지 보수 용이 간단한 애플리케이션에서 유지 보수가 덜 필요
테스트 용이성 독립적인 테스트가 필요한 경우 테스트가 크게 중요하지 않은 경우
트랙잭션 관리 트랜잭션 관리를 필요로 하는 경우 트랜잭션 관리가 필요 없는 경우
계층화된 아키텍처 MVC 패턴 등 계층화된 아키텍처를 선호하는 경우 단일 클래스 또는 구조화된 아키텍처가 불필요

 

public interface MemberService {

    Member saveMember(Member theMember);
}
@Service
@Log4j2
@RequiredArgsConstructor
@Transactional
public class MemberServiceImpl implements MemberService, UserDetailsService {

    private final MemberRepository memberRepository;

    @Override
    public Member saveMember(Member theMember) {

        Member tempMember = memberRepository.findByEmail(theMember.getEmail());
        if (tempMember != null) {
            throw new IllegalStateException("Already Registered Member!");
        }

        return memberRepository.save(theMember);
    }

}

 

특성 단일 클래스 인터페이스 + 구현체
구조 단순하지만 커질 수 있음 명확하게 구조화됨
유지보수 어려울 수 있음 용이함
테스트 어려울 수 있음 용이함 (모킹 가능)
확장성 낮음 높음
의존성 주입 어려울 수 있음 쉬움

 

 

  • @Autowired는 빈에 생성자가 1개이고 생성자의 파라미터 타입이 빈으로 등록 가능하면 사용 안해도 의존성 주입 가능..
  • 이미 가입된 회원은 IllegalStateException 예외 발생

 

회원 가입 기능 테스트


  • 마우스 오른쪽 버튼 - Go To - Test 자동생성 기능 사용
@SpringBootTest
@Transactional
@Log4j2
@RequiredArgsConstructor
@TestPropertySource(locations = "classpath:application-test.properties")
class MemberServiceImplTest {

    @Autowired
    MemberService memberService;

    @Autowired
    PasswordEncoder passwordEncoder;

    @Test
    @DisplayName("회원가입 테스트")
    public void saveMemberTest() {

        MemberFormDTO theMemberDTO = MemberFormDTO.builder()
                .email("test@email.com")
                .name("Dora")
                .address("Daegu")
                .password("1234")
                .build();

        // 비밀번호 인코딩 및 회원 생성
        Member member = Member.createMember(theMemberDTO, passwordEncoder);

        // 회원 저장
        Member savedMember = memberService.saveMember(member);

        // 로그로 저장된 Member 확인
        log.info("Saved Member: {}", savedMember);
        assertNotNull(savedMember.getMemberId()); // ID가 null이 아닌지 확인
        assertEquals("test@email.com", savedMember.getEmail());

    }

    @Test
    @DisplayName("중복 회원 가입 테스트")
    public void saveDuplicateMember() {

        // 객체 생성
        Member member1 = new Member(null, "Dora", "test@email.com", "1234", "Daegu", ADMIN);
        Member member2 = new Member(null, "Dora", "test@email.com", "1234", "Daegu", USER);

        // 객체 저장
        memberService.saveMember(member1);

        // 예외 처리
        IllegalStateException thrown = assertThrows(IllegalStateException.class, () -> {
            memberService.saveMember(member2);
        });

        assertTrue(thrown.getMessage().contains("Already Registered Member!"));
    }
}

 

  • h2 db에 테스트 할 거기 때문에 기존 properties 외 새로 하나 더 만들어서 경로를 지정해줬다.
  • Junit의 Assertions 클래스의 assertEquals 메소드를 이용해서 저장하려고 요청했던 값과 실제 저장된 데이터를 비교함.
  • assertThrows 메소드 이용하면 예외 처리 테스트가 가능.

https://junit.org/junit5/docs/5.0.1/api/org/junit/jupiter/api/Assertions.html

 

Assertions (JUnit 5.0.1 API)

Asserts that all supplied executables do not throw exceptions. If any supplied Executable throws an exception (i.e., a Throwable or any subclass thereof), all remaining executables will still be executed, and all exceptions will be aggregated and reported

junit.org

 

https://velog.io/@roycewon/JUnit-Assert-Methods1

 

JUnit - Assert Methods(1)

JUnit 5 모듈인 Jupiter는 JUnit4에 있는 Assertion method를 포함하여 여러 메소드를 제공한다. Java8에서 추가된 람다와 함께 사용하면 좋다.Assert method는 org.junit.jupiter.api.Assertions 라는 클래

velog.io

 

 

 

Controller 코드 구현


  • controller 패키지 아래 MemberController 생성
@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()) {
            System.out.println("Binding results: " + bindingResult.toString());
            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:/";
    }
}

 

  • ModelmemberFormDTO라는 이름으로 새로운 MemberFormDTO 객체를 추가. 뷰에서 이 객체를 사용하여 폼을 렌더링할 수 있다.
  • localhost:8080/members/new 접속하면 member 패키지 아래에 있는 memberForm.html 화면이 출력됨.
  • @PostMapping은 form 을 제출했을 때 실행되는 코드~ 페이지 구현하고 작성해도 됨!

 

 

회원가입 페이지 구현


  • templates 패키지 아래 member 패키지 생성 - memberForm.html 생성
  • Thymeleaf 템플릿 사용해서 작성함.
  • fragments 패키지 아래에 header/footer html 만들어서 layout01.html에 fragment로 설정하고 content영역만 따로 페이지 구현해서 연동되도록 만들었습니닷,

2024.05.09 - [Spring & Spring Boot] - [Spring Boot] Thymeleaf

https://github.com/SominY/Dora-Flower-Shop/blob/main/src/main/resources/templates/member/memberForm.html(소스코드)

 

Dora-Flower-Shop/src/main/resources/templates/member/memberForm.html at main · SominY/Dora-Flower-Shop

쇼핑몰 프로젝트 (개인 프로젝트). Contribute to SominY/Dora-Flower-Shop development by creating an account on GitHub.

github.com

 

 

 

 

 

 

 

내용 참고: 책 '스프링 부트 쇼핑몰 프로젝트 with JPA'

+ Recent posts