페스트캠퍼스 프로젝트

[어드민] 데이터베이스 접근 로직

hyeongjune 2025. 1. 8. 15:49

패스트 캠퍼스 프로젝트
Github Issue
terminal 에서 mysql 실행

create database admin;
use admin;
grant all on `admin`.* to 'uno'@'localhost' with grant option; //admin 은 빽틱으로 검색한다.
flush privileges;

intellij 에서의 Data source 설정

데이터 베이스는 공란으로 설정한다 => 멀티 모듈 프로젝트 이기 때문에 board 와 admin 데이터 베이스를 모두 사용 할 수 있는 효과

설정후 intellij Database

UserAccountRepository 생성

package com.fastcampus.projectboardadmin.repository;

import com.fastcampus.projectboardadmin.domain.UserAccount;
import org.springframework.data.jpa.repository.JpaRepository;

public interface UserAccountRepository extends JpaRepository<UserAccount, String> {
}

Spring Data Jpa 란?

jpa는 인터페이스로서 자바 표준 명세서 이다. 인터페이스인 JPA를 사용 하기 위해서는 구현체가 필요한데, 대표적으로 Hibernate, Eclipse Link 등이 있다. 하지만 Spring 에서는 JPA를 사용할 때 이 구현체들을 모두 다루지 않고, 구현체들을 좀 더 쉽게 사용하고자 추상화 시킨 Spring Data JPA 라는 모듈을 이용하여 JPA 기술을 다룬다. 

JPA <-> Hibernate <-> Spring Data Jpa

 

Auditing 자동 필드 설정

@EnableJpaAuditing
@Configuration
public class JpaConfig {
    @Bean
    public AuditorAware<String> auditorAware(){
        return () -> Optional.ofNullable(SecurityContextHolder.getContext())
                .map(SecurityContext::getAuthentication)
                .filter(Authentication::isAuthenticated)
                .map(Authentication::getPrincipal)
                .map(BoardAdminPrincipal.class::cast)
                .map(BoardAdminPrincipal::getUsername);
    }
}

@EnableJapAuditing

Jpa에서는 Audit 이라는 기능을 제공한다. Spring Data Jpa 에서 시간, 작성자에 대해 자동으로 값을 넣어주는 기능이다. @CreatedDate, @LastModifiedDate, @CreatedBy, @LastModifiedBy 와 같은 Jpa Auditing 관련 어노테이션을 활성화 한다.

 

@Bean

spring의 DI Container에 의해 관리되는 POJO 를 Bean 이라고 부르며, 이러한 빈들은 Spring 을 구성하는 핵심 요소이다.

POJO로써 Spring 애플리케이션을 구성하는 핵심 객체이다.

Spring IoC 컨테이너(또는 DI 컨테이너) 에 의해 생성 및 관리된다.

class(Bean 으로 등록할 java class), id(Bean의 고유 식별자), scope(Bean을 생성하기 위한 방법), constructor-arg(Bean 생성 시 생성자에 전달 할 파라미터), Propoerty(Bean 생성 시 Setter에 전달 할 인수)을 주요 속성으로 지닌다.

 

[@configuration @Bean]

수동으로 스프링 컨테이너에 빈을 등록하는 방법

개발자가 직접 제어가 불가능한 라이브러리를 빈으로 등록할 때 불가피하게 사용

유지보수성을 높이기 위해 애플리케이션 전 범위적으로 사용되는 클래스나 다형성을 활용하여 여러 구현체를 빈으로 등록 할 때 사용

1개 이상의 @Bean 을 제공하는 클래스의 경우 반드시 @Configuration을 명시해 주어야 싱글톤이 보장됨.

 

SecurityContextHolder

SecurityContext객체를 보관하고 있는 wrapper 클래스

ThreadLocal의 전략 설정

 

SecurityContext

Authentication 객체가 보관되는 저장소 역할

필요시 SecurityContext로 부터 Authentication 객체를 꺼내 사용

각 스레드마다 할당되는 고유 공간인 ThreadLocal 에 저장 되기 때문에 동일한 쓰레드인 경우 필요한 아무 곳에서나 참조 가능

 

Authentication

접근 주체의 인증 정보와 권한을 담는 인터페이스

스프링 시큐리티에서는 인증 시 id와 password를 이용한 credential 기반의 인증을 사용하며, 인증 후 최종 결과를 담아 SecurityContext에 보관되고 필요할 때 전역적으로 참조가 가능

 

테스트 데이터 설정

resources -> data.sql

insert into user_account (user_id, user_password, role_types, nickname, email, memo, created_at, created_by, modified_at, modified_by) values
('uno', '{noop}asdf1234', 'ADMIN', 'Uno', 'uno@mail.com', 'I am Uno.', now(), 'uno', now(), 'uno'),
('mark', '{noop}asdf1234', 'MANAGER', 'Mark', 'mark@mail.com', 'I am Mark.', now(), 'uno', now(), 'uno'),
('susan', '{noop}asdf1234', 'MANAGER,DEVELOPER', 'Susan', 'Susan@mail.com', 'I am Susan.', now(), 'uno', now(), 'uno'),
('jim', '{noop}asdf1234', 'USER', 'Jim', 'jim@mail.com', 'I am Jim.', now(), 'uno', now(), 'uno');

테스트용이지만, 비밀번호가 노출된 데이터 세팅, {noop}은 테스트 용으로 암호화를 적용하지 않겠다는 설정(나중에 개선해 줘야함)

 

JPA Repository 테스트 작성

package com.fastcampus.projectboardadmin.repository;

import com.fastcampus.projectboardadmin.domain.UserAccount;
import com.fastcampus.projectboardadmin.domain.constant.RoleType;
import jakarta.persistence.EntityNotFoundException;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.data.domain.AuditorAware;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.Optional;
import java.util.Set;

import static org.assertj.core.api.AssertionsForClassTypes.assertThat;


@DisplayName("Jpa 연결 테스트")
@Import(JapRepositoryTest.TestJapConfig.class)
@DataJpaTest
class JapRepositoryTest {

    private final UserAccountRepository userAccountRepository;

    public JapRepositoryTest(@Autowired UserAccountRepository userAccountRepository) {
        this.userAccountRepository = userAccountRepository;
    }

    @DisplayName("회원 정보 select 테스트")
    @Test
    void givenUserAccounts_whenSelecting_thenWorksFine() throws Exception {
        //given

        //when
        List<UserAccount> userAccounts = userAccountRepository.findAll();

        //then
        Assertions.assertThat(userAccounts)
                .isNotNull()
                .hasSize(4);
    }

    @DisplayName("회원 정보 insert test")
    @Test
    void givenUserAccount_whenInserting_thenWorksFine() throws Exception {


        //given
        long previousCount = userAccountRepository.count();

        UserAccount userAccount = UserAccount.of("test", "pw", Set.of(RoleType.DEVELOPER), null, null, null);

        //when
        userAccountRepository.save(userAccount);

        //then
        Assertions.assertThat(userAccountRepository.count()).isEqualTo(previousCount + 1);

    }


    @DisplayName("회원 정보 update 테스트")
    @Test
    void givenUserAccountAndRoleType_whenUpdating_thenWorksFine() throws Exception {
        //given
        UserAccount userAccount = userAccountRepository.getReferenceById("uno");
        //when
        userAccount.addRoleType(RoleType.DEVELOPER);

        //then
        userAccount.addRoleTypes(List.of(RoleType.USER, RoleType.USER));

        userAccount.removeRoleType(RoleType.ADMIN);

        UserAccount updatedAccount = userAccountRepository.saveAndFlush(userAccount);

        Assertions.assertThat(updatedAccount)
                .hasFieldOrPropertyWithValue("userId", "uno")
                .hasFieldOrPropertyWithValue("roleTypes", Set.of(RoleType.DEVELOPER, RoleType.USER));

    }

    @Transactional
    @DisplayName("회원 정보 delete 테스트")
    @Test
    void givenUserAccount_whenDeleting_thenWorksFine() throws Exception {

        long previousCount = userAccountRepository.count();

        UserAccount userAccount = userAccountRepository.findById("uno")
                .orElseThrow(() -> new EntityNotFoundException("User not found!"));

        // When
        userAccountRepository.delete(userAccount);

        // Then
        assertThat(userAccountRepository.count()).isEqualTo(previousCount - 1);
    }


    @EnableJpaAuditing
    @TestConfiguration
    static class TestJapConfig{

        @Bean
        AuditorAware<String> auditorAware(){
            return () -> Optional.of("uno");
        }
    }
}

@DataJpaTest

Spring Data Jpa에 대한 테스트를 위한 slice test 어노테이션 이다. @DataJpaTest는 database 및 jpa 와 관련된 자동 설정을 로딩한다. 즉 JpaConfig 를 읽지 않아 테스트 전용 configuration 을 따로 만들어 줘야 한다.

 

회원 정보 delete test에서의 Trouble shooting

userAccountRepository.getreferenceById 를 하고 userAccountRepository.delete(userAccount)을 할 경우 에러가 났다. 

getreferenceById 의 경우 지연 로딩을 이용하여 엔티티를 실제로 사용 할때까지 데이터 베이스 조회를 지연한다. 

hibernate가 이를 관리 중인지 확신 할 수 없어, 삭제 실패 => findById 로 변경

 

이로서 데이터 접근 로직 테스트 정의까지 공부하고 실습하고 기록하였다.