ItemService 코드 추가


  • 등록된 상품을 불러오는 메소드 추가
    @Override
    @Transactional(readOnly = true)
    public ItemFormDTO getItemDtl(Long itemId) {

        // 해당 상품 이미지 조회
        List<ItemImg> itemImgList =
                itemImgRepository.findByItemIdOrderByIdAsc(itemId);

        // 조회한 ItemImg 엔티티를 ItemImgDTO 객체로 만들어 리스트 추가
        List<ItemImgDTO> itemImgDTOList = new ArrayList<>();
        for (ItemImg itemImg : itemImgList) {
            ItemImgDTO itemImgDTO = ItemImgDTO.of(itemImg);
            itemImgDTOList.add(itemImgDTO);
        }

        // 상품 엔티티 조회
        Item item = itemRepository.findById(itemId)
                .orElseThrow(EntityNotFoundException::new);
        ItemFormDTO itemFormDTO = ItemFormDTO.of(item);
        itemFormDTO.setItemImgDTOList(itemImgDTOList);

        return itemFormDTO;
    }

 

  • @Transactional(readOnly = true) : 읽기 전용으로 설정하면 JPA가 더티체킹(변경감지)을 수행하지 않아서 성능을 향상시킬 수 있다.

 

 

ItemController 코드 추가


  • 상품 수정 페이지 진입 위한 코드 추가 - 등록용 페이지와 수정용 페이지 나눠서 개발함.
    @GetMapping( "/admin/item/{itemId}")
    public String itemDtl(@PathVariable("itemId") Long itemId, Model model) {

        try {
            ItemFormDTO itemFormDTO = itemService.getItemDtl(itemId);
            model.addAttribute("itemFormDTO", itemFormDTO);
        } catch (EntityNotFoundException e) {
            model.addAttribute("errorMessage", "The product does not exist.");
            model.addAttribute("itemFormDTO", new ItemFormDTO());
            return "item/itemForm";
        }

        return "item/itemModify";
    }

 

 

ItemImgService 클래스 수정


    @Override
    public void updateItemImg(Long itemImgId, MultipartFile itemImgFile)
            throws Exception {

        if (!itemImgFile.isEmpty()) {
            ItemImg savedItemImg = itemImgRepository.findById(itemImgId)
                    .orElseThrow(EntityNotFoundException::new);

            // 기존 이미지 파일 삭제
            if(!StringUtils.isEmpty(savedItemImg.getImgName())) {
                fileService.deleteFile(itemImgLocation+"/"+savedItemImg.getImgName());
            }

            // 업데이트 파일 업로드
            String oriImgName = itemImgFile.getOriginalFilename();
            String imgName = fileService.uploadFile(itemImgLocation, oriImgName, itemImgFile.getBytes());
            String imgUrl = "/images/item/" + imgName;
            savedItemImg.updateItemImg(oriImgName, imgName, imgUrl);
        }

    }

 

  • savedItemImg 엔티티는 현재 영속 상태이므로 데이터를 변경하는 것만으로 변경 감지 기능이 동작하여 트랜잭션이 끝날 때 update 쿼리가 실행된다.

 

 

Item 클래스 코드 추가


  • 상품 데이터를 업데이트 하는 로직 추가
    public void updateItem(ItemFormDTO itemFormDTO) {

        this.itemNm = itemFormDTO.getItemNm();
        this.price = itemFormDTO.getPrice();
        this.stockNum = itemFormDTO.getStockNum();
        this.itemDetail = itemFormDTO.getItemDetail();
        this.itemSellStatus = itemFormDTO.getItemSellStatus();

    }

 

 

ItemService 업데이트 코드 추가


    @Override
    public Long updateItem(ItemFormDTO itemFormDTO, List<MultipartFile> itemImgFileList)
            throws Exception {

        // 상품 수정
        Item item = itemRepository.findById(itemFormDTO.getId())
                .orElseThrow(EntityNotFoundException::new);
        item.updateItem(itemFormDTO);

        List<Long> itemImgIds = itemFormDTO.getItemImgIds();

        // 이미지 등록
        for (int i=0; i<itemImgIds.size(); i++) {
            itemImgService.updateItemImg(itemImgIds.get(i), itemImgFileList.get(i));
        }

        return item.getId();
    }

 

  • 상품 등록 화면으로부터 전달 받은 상품 아이디를 이용하여 상품 엔티티 조회
  • 상품 등록 화면으로부터 전달 받은 ItemFormDTO를 통해 상품 엔티티를 업데이트
  • 상품 이미지 아이디 리스트 조회
  • 상품 이미지 업데이트 위해 updateItemImg() 메소드에 상품 이미지 아이디와 상품 이미지 파일 정보를 파라미터로 전달

 

ItemController 코드 추가


  • 상품 수정하는 URL을 ItemController 클래스에 추가
    @PostMapping("/admin/item/{itemId}")
    public String itemUpdate(@Valid ItemFormDTO itemFormDTO,
                             BindingResult bindingResult,
                             @RequestParam("itemImgFiles") List<MultipartFile> itemImgFileList,
                             Model model) {

        if (bindingResult.hasErrors()) {
            return "item/itemModify";
        }

        if (itemImgFileList.get(0).isEmpty() && itemFormDTO.getId() == null) {
            model.addAttribute("errorMessage", "The first product image is a required field.");
            return "item/itemModify";
        }

        try {
            itemService.updateItem(itemFormDTO, itemImgFileList);
        } catch (Exception e) {
            model.addAttribute("errorMessage", "An error occured during product registration.");
            return "item/itemForm";
        }

        return "redirect:/";

    }

 

 

 

 

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


 

🤓  UserDetailsService 구현


로그인 기능 구현 전에 먼저 UserDetailsService 인터페이스를 설정해야 된다!

 -> DB에서 회원 정보를 가져오기 위함!

 

유데미 클래스에서는 UserDetailsManager만 배웠는데,, 책에서는 UserDetailsService를 구현해서 무슨 차이인지 찾아봄..

UserDetailsService  VS.  UserDetailsManager

 

UserDetailsService는 Spring Security에서 사용자 정보를 가져오는 데 사용되는 인터페이스.

주요 메소드: loadUserByUsername(String username)

용도: 사용자 정보를 로드하기 위해 주로 로그인 과정에서 사용됨. 즉, 사용자가 로그인 시 입력한 사용자 이름을 기반으로 해당 사용자 정보를 데이터베이스에서 조회하는 역할.

 

UserDetailsManager는 UserDetailsService의 확장된 버전으로, 사용자 정보를 조회하는 것 외에도 사용자 추가, 수정, 삭제 등의 작업을 지원하는 인터페이스!

주요 메소드:

  • createUser(UserDetails user) 

  • updateUser(UserDetails user)

  • deleteUser(String username)

  • userExists(String username)

용도: 사용자 정보를 CRUD(생성, 읽기, 업데이트, 삭제) 작업을 지원하며, 더 복잡한 사용자 관리 작업이 필요한 경우 사용됨

가장 많이 사용되는 구현체는 JdbcUserDetailsManager와 InMemoryUserDetailsManager

 -> JdbcUserDetailsManager를 구현하려 하다 user, authority 테이블 필요하다 해서.. 일단 나중에 refactoring 하면서 고쳐보기로함.. 🧐

 

  • Service 패키지 아래 UserDetailsService를 구현한 CustomUserDetailsService 클래스 생성
@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {

    public final MemberRepository memberRepository;

    @Override
    @Transactional
    public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
        Member theMember = memberRepository.findByEmail(email);

        if(theMember == null) {
            throw new UsernameNotFoundException(email);
        }

        return User.builder()
                .username(theMember.getEmail())
                .password(theMember.getPassword())
                .roles(theMember.getRole().toString())
                .build();

    }
}

 

  - email로 로그인 하기 때문에 파라미터를 email로 넘겨줌.!

  - 존재하지 않는 계정이면 예외처리.

  - User 객체로 변환하여 반환

  - 원래 책에는 MemberService 안에 넣어 줬는데, 나는 아예 따로 빼줌,, remember me 기능 구현에도 쓰이기 때문...!

 

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    private final CustomUserDetailsService customUserDetailsService;

    // 생성자 주입을 사용하여 의존성 주입
    public SecurityConfig(CustomUserDetailsService customUserDetailsService) {
        this.customUserDetailsService = customUserDetailsService;
    }
    
     @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

        http.formLogin(form ->
                form.loginPage("/members/login")
                        .defaultSuccessUrl("/")
                        .usernameParameter("email")
                        .failureUrl("/members/login/error")
        );

        http.logout(logout ->
                logout.logoutRequestMatcher(new AntPathRequestMatcher("/members/logout"))
                        .logoutSuccessUrl("/")
        );
    
    ...

 

  💡 Spring Framework에서는 @Autowired 보다 생성자 주입을 권장하고 있다. 이는 코드의 가독성을 높이고, 의존성 관리의 명확성을 보장.

 

  - Spring Security의 인증 과정에서 사용자의 로그인 요청이 들어오면, Spring Security는 UserDetailsServiceloadUserByUsername 메서드를 호출하여 사용자 정보를 조회.

 

 

 

로그인 페이지 생성


  • templates/member/memberLoginForm.html 생성
    <form role="form" method="post" action="/members/login" novalidate>

        <i class="bi bi-chat-square-heart-fill fs-1" style="color: #FFCFE2;"></i>
        <h1 class="h3 mt-1 mb-3 fw-bold text-muted">Please sign in</h1>

        <div class="form-floating">
            <input type="email" name="email" class="form-control"
                   id="floatingInput" placeholder="name@example.com">
            <label th:for="email">Email address</label>
        </div>

        <div class="form-floating">
            <input type="password" class="form-control" name="password"
                   id="floatingPassword" placeholder="Password">
            <label th:for="password">Password</label>
        </div>

        <div th:if="${loginErrorMsg}" class="error" th:text="${loginErrorMsg}"
             style="color: red; font-size: 15px"></div>

        <div class="form-check text-start my-3">
            <input class="form-check-input" type="checkbox" id="rememberMe" name="rememberMe">
            <label class="form-check-label" for="rememberMe">
                Remember me
            </label>
        </div>

        <button class="btn btn-light w-100 py-2 fw-bold text-muted" type="submit"
                style="background-color: #FFCFE2;">Sign in
        </button>
        <p class="mt-3 text-body-secondary">&copy; 2024</p>

    </form>

 

  - remember me 를 추가해서 브라우저를 껐다 켜도 로그인 상태 유지하도록 설정할 예정!

  

 

부트스트랩 모달창을 띄워서 로그인하고 싶었는데, ajax 작동이 잘 안됨.. 콘솔에 에러도 안뜨고 내용도 안 불러와져서 일단 패스..

부트스트랩 문제인지를 잘 모르겠다,, 다른 블로그 글 다 뒤져봤는데두 안나옴 ㅠ,ㅠ..data-bs-toggle과 data-bs-target 속성지정도 제대로 했고...onclick 속성 사용하면 ReferenceError: openLoginModal is not defined at HTMLAnchorElement.onclick 에러뜸 대환장 파티,,시간 너무 오래걸려 일단 나중에 다시 해보기로..ㅎ

 

 

 

MemberController 추가 코드 구현


  • 회원가입 코드 구현한 곳 아래에 추가
    @GetMapping("/login")
    public String loginMember() {
        return "/member/memberLoginForm";
    }

    @GetMapping("/login/error")
    public String loginError(Model model) {
        model.addAttribute("loginErrorMsg", "Please enter a valid email or password");
        return "/member/memberLoginForm";
    }

 

- SecurityConfig에 로그인 성공시 리다이렉트 될 Url을 설정해줘서 PostMapping은 따로 작성 x

   -> 찾아보니 로그인 관련 설정을 SecurityConfig에서 한 곳에서 관리할 수 있어 보안 설정과 관련된 모든 구성 요소를 쉽게 찾을 수 있어서 이렇게 쓰이는듯!

 

- login/error 경로에 대한 GET 요청이 들어오면 loginError 메서드가 호출 -> 이 메서드는 Model에 오류 메시지를 추가하고 /member/memberLoginForm 템플릿을 반환 -> 렌더링 후 loginErrorMsg가 존재하면 <div> 요소가 표시되고, 오류 메시지가 사용자에게 빨간색으로 나타남

 

 

 

Test Code 작성


@SpringBootTest
@AutoConfigureMockMvc
@Transactional
@TestPropertySource(locations = "classpath:application-test.properties")
class MemberControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    MemberService memberService;

    @Autowired
    PasswordEncoder passwordEncoder;

    public Member createMember(String email, String password) {
        MemberFormDTO memberFormDTO = MemberFormDTO.builder()
                .name("홍길동")
                .email(email)
                .password(password)
                .address("서울시 마포구 합정동")
                .build();

        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);
        mockMvc.perform(formLogin().userParameter("email")
                        .loginProcessingUrl("/members/login")
                        .user(email).password(password))
                .andExpect(SecurityMockMvcResultMatchers.authenticated());
    }

    @Test
    @DisplayName("로그인 실패 테스트")
    public void loginFailTest() throws Exception {
        String email = "test@email.com";
        String password = "1234";

        this.createMember(email, password);
        mockMvc.perform(formLogin().userParameter(email)
                .loginProcessingUrl("/members/login/error")
                .user(email).password("12345"))
                .andExpect(SecurityMockMvcResultMatchers.unauthenticated());

    }

}

 

https://adjh54.tistory.com/347

 

[Java] Spring Boot MockMvc 이해하기 : 테스트 흐름 및 사용예제

해당 글에서는 MockMvc에 대해 이해하고 활용하는 방법에 대해 확인해 봅니다.   💡 [참고] 이전에 작성한 Test 관련 글들을 읽으시면 도움이 됩니다.분류링크JUnit 5 이론 및 구성 요소https://adjh54.

adjh54.tistory.com

https://docs.spring.io/spring-security/reference/servlet/test/mockmvc/result-matchers.html

 

SecurityMockMvcResultMatchers :: Spring Security

At times it is desirable to make various security related assertions about a request. To accommodate this need, Spring Security Test support implements Spring MVC Test’s ResultMatcher interface. In order to use Spring Security’s ResultMatcher implement

docs.spring.io

 

MockMvc 라이브러리 라는 것도 있었다는 것을 처음 깨달은,,🐰

테스트용으로 많이 쓰인다고 한다!

 

perform() : MockMvc를 사용하여 HTTP 요청을 실행

andExpect() : 컨트롤러의 응답을 검증

 

 

 

Remember Me 기능 구현


이왕 부트스트랩 디자인 긁어온거 책에는 안 나와있는 remember 기능 구현 해보자 싶어서 ... 🤓

  http.rememberMe(rememberMe -> rememberMe
                .rememberMeParameter("rememberMe") // default: remember-me, checkbox 등의 이름과 맞춰야함
                .tokenValiditySeconds(3600) // 쿠키의 만료시간 설정(초), default: 14일
                .alwaysRemember(false)); // 사용자가 체크박스를 활성화하지 않아도 항상 실행, default: false
//                .userDetailsService(customUserDetailsService)); // 기능을 사용할 때 사용자 정보가 필요함. 반드시 이 설정 필요함.

 

일단 SecurityFilterChain 안에 이 코드를 넣어줬다!

근데 작동이 안돼서 왜지.. 했는데 html에 input 코드에 id, name은 'rememberMe'로 설정 유지하고 기본적으로 설정되어 있던 value값을 삭제하니깐 작동이 되더라는... ;

 

 

버전 업그레이드 하면서 SecurityFilterChain 사용시 userDetailService 설정안해줘도 된다고 한다!!!

 

 

https://velog.io/@dailylifecoding/spring-security-remember-me

 

[Spring Security] Remember Me 인증

스프링 시큐리티가 제공하는 Remember Me 기능을 알아보자.

velog.io

 

 

remember-me 쿠키가 생성되어야 성공!

 

 

 

 

 

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


개발환경

 

  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'


1.  Spring Data JPA

JDBC에서 작업하다가 MyBatis를 배울 경우에는 SQL 작성법도 거의 동일하고, 코드의 간략화가 목표여서 러닝커브가 높지 않지만
JPA의 경우에는 JDBC, MyBatis와는 사용방법이 상당한 차이가 있고, 추상적인 개념이 많이 포함이 되어서 러닝커브가 상당히 높음.

그리고 SQL문을 직접 만드는 것보다 JPA가 만드는 SQL의 효율이 떨어진다거나, 동일한 작업인데 JPA가 더 많은 SQL문을 사용하는 비효율성이 존재. 하지만 MSA 방식으로 애플리케이션을 설계하는 경향이 높아짐에 따라, JPA를 사용하는 작업이 많아짐.


1)  ORM / JPA

우리가 사용하는 대부분의 프로그램은 사용자가 입력한 데이터나 비지니스 로직 수행 결과로 얻은 데이터를 재사용할 수 있도록 데이터베이스에 저장을 함. 하지만 자바의 객체와 데이터베이스의 테이블이 정확하게 일치하지는 않음. 따라서 둘 사이를 매핑하기 위해서 많은 SQL 구문과 자바 코드가 필요해짐.

💡  ORM은 정확하게 일치하지 않는 자바 객체와 테이블 사이를 매핑 

  ✓  자바 객체에 저장된 데이터를 테이블의 Row 정보로 저장하고. 반대로 테이블에 저장된 Row 정보를 자바 객체로 매핑

  ✓  이 과정에서 사용되는 SQL 구문과 자바 코드를 ORM 프레임워크가 자동으로 만들어줌.

어떤 DB 연동 기술이나 프레임워크를 사용하더라도 SQL 명령어를 자바 클래스나 외부의 XML 파일에 작성해야 함. 그리고 작성된 SQL은 유지보수 과정에서 지속적으로 수정되며 새로운 SQL이 추가되기도 함.

 

💡  ORM 프레임워크의 가장 큰 특징이자 장점은 DB 연동에 필요한 SQL을 자동으로 생성
  ✓  이렇게 생성된 SQL은 DBMS가 변경될때 자동으로 변경이 됨

        ➡️  다만 ORM 환경 설정 파일에서 DBMS가 변경된 것을 알려주어야 함

 

ORM ; Object Relational Mapping
객체 관계 매핑
  • 자바와 같은 객체지향 언어에서 의미하는 객체와 RDB Relational Database 의 테이블을 자동으로 매핑하는 방법
  • 클래스는 데이터베이스의 테이블과 매핑하기 위해 만들어진 것이 아니기 때문에 RDB 테이블과의 불일치가 존재
  • ORM이 이 둘의 불일치와 제약사항을 해결하는 역할. ORM을 이용하면 쿼리문 작성이 아닌 코드(메서드)로 데이터를 조작할 수 있음

 

JPA ; Java Persistence API
  • 자바 진영의 ORM 기술 표준으로 채택된 인터페이스 모음
  • ORM이 큰 개념이라면 JPA는 더 구체화된 스펙을 포함. 즉, JPA 또한 실제로 동작하는 것이 아니고 어떻게 동작해야 하는지 매커니즘을 정리한 표준 명세
  • JPA를 구현한 대표적인 구현체로 Hibernate, EclipseLink, DataNucleus, OpenJpa 등이 있음
  • JPA 인터페이스를 구현한 가장 대표적인 오픈소스가 Hibernate이고 실질적인 기능은 하이버네이트에 구현되어 있음

 

Spring Data JPA
  • JPA를 편리하게 사용할 수 있도록 지원하는 스프링 하위 프로젝트 중 하나
  • CRUD 처리에 필요한 인터페이스를 제공하며, 하이버네이트의 엔티티 매니저를 직접 다루지 않고 repository를 정의해 사용함으로써 스프링에 적합한 쿼리를 동적으로 생성하는 방식으로 데이터베이스를 조작
  • 이를 통해 하이버네이트에서 자주 사용되는 기능을 더 쉽게 사용할 수 있게 구현한 라이브러리

 

JPA 사용 시 장점


    A.  특정 데이터베이스에 종속되지 않음


        ✓  오라클을 MariaDB로 변경한다면 데이터베이스마다 쿼리문이 다르기 때문에 전체를 수정해야 함. 따라서 처음 선택한 데이터베이스를 변경하기 어려움. 하지만 JPA는 추상화한 데이터 접근 계층을 제공함. 설정 파일에 어떤 데이터베이스를 사용하는지 알려주면 얼마든지 데이터베이스를 변경할 수 있음

    B.  객체지향적 프로그램

      ✓  데이터베이스 설계 중심의 패러다임에서 객체지향적으로 설계가 가능. 이를 통해 좀 더 직관적이고 비지니스 로직에 집중


    C.  생산성 향상

 

      ✓  데이터베이스 테이블에 새로운 컬럼이 추가 되었을 경우, 해당 테이블의 컬럼을 사용하는 DTO 클래스의 필드도 모두 변경해야 함.
      ✓  JPA에서는 테이블과 매핑된 클래스에 필드만 추가한다면 쉽게 관리가 가능. 또한 SQL문을 직접 작성하지 않고 객체를 이용하여 동작하기 때문에 유지보수 측면에서 좋고 재사용성도 증가.

 

JPA 사용 시 단점


    A.  복잡한 쿼리 처리


      ✓  통계 처리 같은 복잡한 쿼리를 사용할 경우는 SQL문을 사용하는 것이 나을 수 있음
      ✓  JPA에서는 Native SQL을 통해 기존의 SQL문을 사용할 수 있지만 그러면 특정 데이터베이스에 종속된다는 단점이 생김. 이를 보완하기 위해서 SQL과 유사한 기술인 JPQL, Querydsl을 지원.

    B.  성능 저하 위험


      ✓  객체 간의 매핑 설계가 잘못했을 때 성능 저하가 발생할 수 있으며, 자동으로 생성되는 쿼리가 많기 때문에 개발자가 의도하지 않은 쿼리로 인해 성능이 저하되기도 함.

    C.  학습 시간


      ✓  JPA를 제대로 사용하려면 알아야 할 것이 많아서 학습하는데 시간이 오래 걸림.


 

2)  엔티티

엔티티 Entity 란 데이터베이스의 테이블에 대응하는 클래스. @Entity가 붙은 클래스는 JPA에서 관리하며 엔티티라고 함.
클래스 자체나 생성한 인스턴스도 엔티티라 부름.

 

엔티티 매니저 팩토리  Entity Manager Factory
  • 엔티티 매니저 인스턴스를 관리하는 주체
  • 애플리케이션 실행 시 한 개만 만들어지며 사용자로 부터 요청이 오면 엔티티 매니저 팩토리로 부터 엔티티 매니저를 생성.
엔티티 매니저  Entity Manager
  • 영속성 컨텍스트에 접근하여 엔티티에 대한 데이터베이스 작업을 제공
  • 내부적으로 데이터베이스 커넥션을 사용해서 데이터베이스에 접근
영속성 컨텍스트  Persistence Context
  • 엔티티를 영구 저장하는 환경으로 엔티티 매니저를 통해 영속성 컨텍스트에 접근

 


2.  Board 엔티티와 JpaRepository

JPA를 이용하는 개발의 핵심은 객체지향을 통해서 영속 계층을 처리하는데 있음. 따라서 JPA를 이용할 때는 테이블과 SQL을 다루는 것이 아니라 데이터에 해당하는 객체를 엔티티 객체로 다루고 JPA로 이를 데이터베이스와 연동해서 관리. 엔티티 객체는 쉽게 말해서 PK 기본키를 가지는 자바의 객체. 엔티티 객체는 고유의 식별을 위해 @Id를 이용해서 객체를 구분하고 관리

Spring Data JPA는 엔티티 객체를 이용해서 JPA를 이용하는데 더욱 편리한 방법들을 제공하는 스프링 관련 라이브러리. 자동으로 객체를 생성하고 이를 통해서 예외 처리 등을 자동으로 처리하는데 이를 위해서 제공되는 인터페이스가 JpaRepository.

💡  개발의 첫 단계은 엔티티 객체를 생성하기 위한 엔티티 클래스를 정의하는 것

 

프로젝트에 domain 패키지를 구성하고 게시물을 위한 Board 엔티티를 작성
@Entity
public class Board {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long bno;

    private String title;

    private String content;

    private String writer;
}

 

  🥸  엔티티 객체를 위한 엔티티 클래스는 반드시 @Entity를 적용해야 하고 @Id가 필요
  🥸  게시물은 데이터베이스에 추가될 때 생성되는 번호 auto increment 를 이용할 것이므로 이런 경우에 '키 생성 전략 key generate strategy 중에 GenerationType.IDENTITY로 데이터베이스에서 알아서 결정하는 방식을 이용

  🚀  GenerationType.IDENTITY : 데이터베이스에 위임(MYSQL / MariaDB) - auto_increment


1) @MappedSuperclass를 이용한 공통 속성 처리

  👾  데이터베이스의 거의 모든 테이블에는 데이터가 추가된 시간이나 수정된 시간 등의 컬럼을 작성
  👾  자바에서는 이를 쉽게 처리하고자 @MappedSuperclass를 이용해서 공통으로 사용되는 칼럼들을 지정하고 해당 클래스를 상속해서 이를 손쉽게 처리.

 

프로젝트의 domain 패키지에 BaseEntity 클래스를 추가
@MappedSuperclass
@EntityListeners(value = {AuditingEntityListener.class})
@Getter
public class BaseEntity {
    @CreatedDate
    @Column(name = "regdate", updatable = false)
    private LocalDateTime regDate;

    @LastModifiedDate
    @Column(name = "moddate")
    private LocalDateTime modDate;

}

 

  🥸  BaseEntity에서 가장 중요한 부분은 자동으로 Spring Data JPA의 AuditingEntityListener를 지정하는 부분

  🥸  AuditingEntityListener는 리스너로 이를 적용하면 엔티티가 데이터베이스에 추가되거나 변경될 때 자동으로 시간 값을 지정

 

AuditingEntityListener를 활성화 시키기 위해서는 프로젝트 설정에 @EnableJpaAuditing을 추가
@SpringBootApplication
@EnableJpaAuditing
public class SpringbootApplication {
	public static void main(String[] args) {
		SpringApplication.run(SpringbootApplication.class, args);
	}
}

 

기존의 Board 클래스는 BaseEntity를 상속하도록 변경하고 추가적인 어노테이션들을 적용
@Entity
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class Board extends BaseEntity {

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

    @Column(length = 500, nullable = false) // 컬럼의 길이와 null 허용 여부
    private String title;

    @Column(length = 2000, nullable = false)
    private String content;

    @Column(length = 50, nullable = false)
    private String writer;
    
}

 


2)  JpaRepository 인터페이스

  👾  Spring Data JPA 를 이용할 때는 JpaRepository라는 인터페이스 선언만으로 데이터베이스 관련 작업을 어느 정도 처리할 수 있음
  👾  개발 단계에서 JpaRepository 인터페이스를 상속하는 인터페이스를 선언하는 것 만으로 CRUD외 페이징 처리가 모두 완료

 

repository 패키지를 생성하고 BoardRepository 인터페이스를 선언
public interface BoardRepository extends JpaRepository<Board, Long> {}

 

  🥸  JpaRepositoy 인터페이스를 상속할 때에는 엔티티 타입과 @Id 타입을 지정해 주어야 하는 점을 제외하면 아무런 코드가 없이도 개발 가능

 


3)  테스트 코드를 통한 CRUD / 페이징 처리 확인

  👾  Spring Data JPA를 이용하면 SQL의 개발도 거의 없고, JPA의 많은 기능을 활용 할 수 있지만 항상 테스트 코드로 동작 여부를 확인하는 것이 좋음

 

프로젝트에 test에 repository 패키지를 추가하고 BoardRepositoryTests 클래스를 추가
@SpringBootTest
@Log4j2
public class BoardRepositoryTests {
    @Autowired
    private BoardRepository boardRepository;
}

 

insert 기능 테스트


  ✓  데이터베이스에 insert를 실행하는 기능은 JpaRepository의 save()를 통해서 이루어짐
  ✓  save()는 현재의 영속 컨텍스트 내에 데이터가 존재하는지 찾아보고 해당 엔티티객체가 없을 때는 insert를, 존재할 때는 update을 자동으로 실행

    @Test
    public void testInsert() {
        for (int i = 1; i <= 100; i++) {
            Board board = Board.builder()
                    .title("title...")
                    .content("content..." + i)
                    .writer("user" + (i % 10))
                    .build();
            Board result = boardRepository.save(board);
            log.info("BNO: " + result.getBno());
        }
    }

 

 

  📍  save()의 결과는 데이터베이스에 저장된 데이터와 동기화된 Board 객체가 반환
  📍  최종적으로 테스트 실행 후에 데이터베이스를 조회해 보면 100개의 데이터가 생성된 것을 확인
  📍  테스트 코드의 경우 @Id 값이 null 이므로 insert만 실행. (update와 비교)

 

 

 

 

 


 

select 기능 테스트


  ✓  특정한 번호의 게시물을 조회하는 기능은 findById()를 이용해서 처리. findById()의 리턴 타입은 Optional<T>

    @Test
    public void testSelect() {
        Long bno = 100L; // 조회하고자 하는 bno 번호
        Optional<Board> result = boardRepository.findById(bno);
        Board board = result.orElseThrow();
        log.info(board);
    }


 

update 기능 테스트


  ✓  update 기능은 insert와 동일하게 save()를 통해서 처리. 동일한 @Id 값을 가지는 개체를 생성해서 처리가능.
  ✓  update는 등록 시간이 필요하므로 가능하면 findById()로 가져온 객체를 이용하여 약간의 수정을 통해서 처리하도록 함
  ✓  일반적으로 엔티티 객체(Board 클래스)는 가능하면 최소한의 변경이나 변경이 없는 immutable로 설계하는 것이 좋지만, 강제적인 사항은 아니므로 Board 클래스에 수정이 가능한 부분을 미리 메서드로 설계

  📍  Board의 경우 '제목 / 내용'은 수정이 가능하므로 이에 맞도록 change() 라는 메서드를 추가

public void change(String title, String content) {
    this.title = title;
    this.content = content;
}

 

  📍  테스트 코드에서는 이를 활용하여 update를 실행하는 테스트 코드를 작성

@Test
public void testUpdate() {
    Long bno = 100L;
    Optional<Board> result = boardRepository.findById(bno);
    Board board = result.orElseThrow();

    board.change("update... title 100", "update content 100");
    boardRepository.save(board);
}

 

findById 실행 ▶️ save()실행 시 다시 검사 ▶️ update 실행

 

  ⚡️  update가 실행된 후에 moddate가 변경된 것 확인
  ⚡️  변경할 부분을 sql에서 set로 지정하는 것이 아니라, 객체를 영속성 콘텍스트에 올리면 @Id와 동일한 데이터를 찾아 매핑을 하는 개념이어서 수정할 부분만 엔티티 객체에 넣으면 안되고, 데이터베이스에 저장되어야 하는 데이터와 동일한 엔티티 객체가 필요함

 

  📍  JDBC나 MyBatis 방식으로 처리하면 에러

   @Test
    public void testUpdate2() {

        Long bno = 100L;
        Board board = Board.builder()
                .bno(bno)
                .title("title...")
                .content("content...update3")
                .build();
        boardRepository.save(board);
    }

 

  📍  없는 @Id 값을 지정하면 update가 아니라 insert가 실행

   @Test
    public void testUpdate3() {

        Long bno = 10000L;
        Board board = Board.builder()
                .bno(bno)
                .title("title...")
                .content("content...update")
                .build();
        boardRepository.save(board);
    }


 

delete 기능 테스트


  ✓ delete는 @Id에 해당하는 값으로 deleteById()를 통해서 실행

  ✓  deleteById() 역시 데이터베이스의 내부에 같은 @Id가 존재하는지 먼저 확인하고 delete문이 실행

@Test
public void testDelete() {
    Long bno = 1L;
    boardRepository.deleteById(bno);
}

 


 

수정이나 삭제 시에 굳이 select문이 먼저 실행되는 이유는 JPA의 동작 방식과 관련. JPA를 이용한다는 것은 엄밀하게 말하면 영속 컨텍스트와 데이터베이스를 동기화해서 관리한다는 의미. 그러므로 특정한 엔티티 객체가 추가되면 영속 컨텍스트에 추가하고, 데이터베이스와 동기화가 이루어짐. 마찬가지로 수정이나 삭제를 한다면 영속 컨텍스트에 해당 엔티티 객체가 존재해야만 하므로 먼저 select로 엔티티 객체를 영속 컨텍스트에 저장해서 이를 삭제한 후에 delete가 이루어짐.


4)  Pageable과 Page<E> 타입

  👾  Spring Data JPA를 이용해서 별도의 코드 없이도 CRUD가 실행되지만, 페이징 처리도 가능
  👾  페이징 처리는 Pageable이라는 타입의 객체를 구성해서 파라미터로 전달하면 됨.
  👾  Pageable은 인터페이스로 설계되어 있고, 일반적으로는 PageRequest.of()라는 기능을 이용해서 개발이 가능.

PageRequest.of(페이지 번호, 사이즈) : 페이지 번호은 0부터
PageRequest.of(페이지 번호, 사이즈, Sort) : 정렬 조건 추가
PageRequest.of(페이지 번호, 사이즈, Sort.Direction, 속성...) : 정렬 방향과 여러 속성 지정

 

  ⚡️  파라미터로 Pageable을 이용하면 리턴 타입은 Page<T> 타입을 이용할 수 있는데 이는 단순 목록뿐 아니라 페이징 처리에 데이터가 많은 경우에는 count 처리를 자동으로 실행
  ⚡️  대부분의 Pageable 파라미터는 메서드 마지막에 사용하고, 파라미터에 Pageable이 있는 경우에는 메서드의 리턴 타입을 Page<T> 타입으로 설계.

 

JpaRepository에는 findAll()이라는 기능을 제공하여 기본적인 페이징 처리를 지원
@Test
public void testPaging() {
    // 1 page order by bno desc
    Pageable pageable = PageRequest.of(0, 10, Sort.by("bno").descending());
    Page<Board> result = boardRepository.findAll(pageable);
}

 

 

  ✓  findAll()의 리턴 타입으로 나오는 Page<T> 타입은 내부적으로 페이징 처리에 필요한 여러 정보를 처리
  ✓  예를 들어 다음 페이지가 존재하는지, 이전 페이지가 존재하는지, 전체 데이터의 개수는 몇 개인지 등의 기능들을 모두 알아낼수 있음

    @Test
    public void testPaging() {
        // 1 page order by bno desc
        Pageable pageable = PageRequest.of(0, 10, Sort.by("bno").descending());
        Page<Board> result = boardRepository.findAll(pageable);

        log.info("total count : " + result.getTotalElements());
        log.info("total page : " + result.getTotalPages()); 
        log.info("page number : " + result.getNumber());
        log.info("page size : " + result.getSize());

        // prev next
        log.info(result.hasPrevious() + ": " + result.hasNext());

        List<Board> boardList = result.getContent();

        boardList.forEach(board -> log.info(board));
    }

 


5)  쿼리 메서드와 @Query

  👾  간단한 CRUD는 JpaRepository를 이용하면 되지만 다양한 검색 조건이 들어가게 되면 추가 기능이 필요하게 됨
  👾  예를 들어 특정한 범위의 Memo 객체를 검색하거나, like 처리가 필요한 경우, 여러 검색 조건이 필요한 경우 등

💡  Spring Data JPA의 경우에는 이러한 처리를 위해 다음과 같은 방법을 제공
    ·  쿼리 메서드 : 메서드의 이름 자체가 쿼리의 구문으로 처리되는 기능.
    ·  @Query : SQL과 유사하게 엔티티 클래스의 정보를 이용해서 쿼리를 작성하는 기능. JPQL.
    ·  Querydsl 등의 동적 쿼리 기능


  ⚡️  쿼리 메서드는 보통 SQL에서 사용하는 키워드와 컬럼들을 같이 결합해서 구성하면 그 자체가 JPA에서 사용하는 쿼리가 되는 기능
        -  일반적으로는 메서드 이름을 'findBy...' 혹은 'get...'으로 시작하고 칼럼명과 키워드를 결합하는 방식으로 구성
            (각 키워드 사용법은 https://spring.io/projects/spring-data-jpa 참고)
        -  인텔리제이 얼티메이트에서는 자동완성 기능으로 쿼리 메서드를 작성할 수 있는 기능을 지원
        -  쿼리 메서드는 실제로 사용하려면 상당히 길고 복잡한 메서드를 작성하게 되는 경우가 많음
             ➡️  예를 들어 '제목'에 특정한 '키워드'가 존재하는 게시글들을 bno의 역순으로 정렬해서 가져오고 싶다면 아래의 메소드 이름이 생성

          ✓  쿼리 메서드는 주로 단순한 쿼리를 작성할 때 사용하고 실제 개발에서는 많이 사용되지 않음

Page<Board> findByTitleContainingOrderByBnoDesc(String keyword, Pageable pageable);


    
  ⚡️  @Query로 JPQL을 이용
      -  @Query 어노테이션의 value로 작성하는 문자열을 JPQL이라고 하는데 JPQL은 SQL과 유사하게 JPA에서 사용하는 쿼리 언어 query language 라고 생각하면 됨
      -  JPA는 데이터베이스에 독립적으로 개발이 가능하므로 특정한 데이터베이스에서만 동작하는 SQL 대신에 JPA에 맞게 사용하는 JPQL을 이용. JPQL은 테이블 대신에 엔티티 타입을 이용하고 컬럼 대신에 엔티티의 속성으로 이용해서 작성. JPQL은 SQL을 대신하는 용도로 사용하기 때문에 SQL에 존재하는 여러 키워드나 기능들이 거의 유사하게 제공.

 

  📍 앞선 쿼리 메서드에 @Query를 이용한다면 다음과 같이 작성

@Query("select b from Board b where b.title like concat('%', :keyword, '%')")
Page<Board> findKeyword(String keyword, Pageable pageable);

 

  ✓  작성된 JPQL을 보면 SQL과 상당히 유사하다는 것을 알 수 있음
  ✓  @Query를 이용하면 크게 쿼리 메서드가 할 수 없는 몇 가지 기능을 할 수 있음
        * 조인과 같이 복잡한 쿼리를 실행할 수 있는 기능
        * 원하는 속성들만 추출해서 Object[]로 처리하거나 DTO로 처리하는 기능
        * nativeQuery 속성값을 true로 지정해서 특정 데이터베이스에서 동작하는 SQL을 사용하는 기능


  📍  native 속성을 지정하는 예제는 다음과 같이 작성 가능

@Query(value="select now()", nativeQuery=true)
String getTime();

 

 

 

 

 

[ 내용 참고 : IT 학원 강의 ]

+ Recent posts