로그인 수정 (권한 구조 변경) 및 테스트 코드 작성

최준호·2022년 5월 21일
0

game

목록 보기
14/14
post-thumbnail

👏로그인 구조 변경

회사에서도 로그인을 만들고 있는데 개인 프로젝트를 진행할 때보다 더 꼼꼼하게 진행하다보니 이래저래 배우는게 많다. 그러다보니 내가 개인적으로 진행했던 코드가 엄청 허접하다는 것을 깨닫는 시간이 많았고 그래서 수정해보려고 한다.

✅1차 문제

@ManyToMany로 구현된 다:대 관계

@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(
        name = "game_user_role"
)
private List<GameRole> roles = new ArrayList<>();

다음과 같이 ManyToMany관계를 @JoinTable 전략을 사용하여 구현해놓았다. 물론 문제는 되지 않고 정상적으로 동작하기도 하고 실제 ManyToMany처럼 작동하진 않게 되어있지만 그래도 직접 다:1 1:다 - 1:다 다:1 관계로 변경해서 사용하고 싶은 생각이 들었다. 그래야 내가 테이블도 컨트롤하기도 쉽고!

변경 전 코드

@Entity
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Table(name = "game_users")
public class GameUserEntity{
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    Long id;

    @Column(name = "user_id", nullable = false, unique = true, length = 10)
    private String userId;
    @Column(nullable = false, length = 20)
    private String name;
    @Column(nullable = false, length = 150)
    private String pw;

    @ManyToMany(fetch = FetchType.EAGER)
    @JoinTable(
            name = "game_user_role"
    )
    private List<GameRole> roles = new ArrayList<>();


    @CreationTimestamp
    private LocalDateTime createdAt = LocalDateTime.now();

    @Builder
    public GameUserEntity(@NonNull String userId,@NonNull String name,@NonNull String pw) {
        this.userId = userId;
        this.name = name;
        this.pw = pw;
    }
}
@Entity
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
public class GameRole implements GrantedAuthority {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

    @Override
    public String getAuthority() {
        return name;
    }
}

변경 후

먼저 ManyToMany로 연관관계를 맺은 엔티티들의 구조를 모두 수정해주어야한다.

GameUser

@Entity
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Table(name = "game_users")
@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "game_user_id")
public class GameUser {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "game_user_id")
    Long gameUserId;

    @Column(name = "user_id", nullable = false, unique = true, length = 10)
    private String userId;
    @Column(nullable = false, length = 20)
    private String name;
    @Column(nullable = false, length = 150)
    private String pw;

    @OneToMany(mappedBy = "id", fetch = FetchType.EAGER)
    @JsonManagedReference(value = "user")
    List<GameUserRoleMapping> gameUserRoleMapping = new ArrayList<>();

    @UpdateTimestamp
    protected LocalDateTime updatedAt = LocalDateTime.now();

    @CreationTimestamp
    private LocalDateTime createdAt = LocalDateTime.now();

    @Builder
    public GameUser(@NonNull String userId, @NonNull String name, @NonNull String pw) {
        this.userId = userId;
        this.name = name;
        this.pw = pw;
    }
}

GameRole

@Entity
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
public class GameRole implements GrantedAuthority {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "game_role_id")
    private Long gameRoleId;
    private String name;

    @OneToMany(mappedBy = "id", fetch = FetchType.LAZY)
    private List<GameUserRoleMapping> gameUserRoleMappings = new ArrayList<>();

    @Override
    public String getAuthority() {
        return name;
    }
}

GameUserRoleMapping

@Entity
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "game_user_role_mapping")
@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
public class GameUserRoleMapping {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "game_user_id")
    @JsonBackReference(value = "user")
    private GameUser gameUser;

    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = "game_role_id")
    private GameRole gameRole;

    @Builder
    public GameUserRoleMapping(GameUser gameUser, GameRole gameRole) {
        this.gameUser = gameUser;
        this.gameRole = gameRole;
    }
}

GameUserRoleMapping 매핑 테이블을 직접 만들어서 내가 수정해서 사용할 수 있게 되었다. 기존에는 JPA에서 자동으로 테이블을 만들어서 매핑해서 사용했기 때문에 내가 컨트롤하긴 어려웠다!

잘 만들어졌고 쿼리문도 보면 연관관계가 잘 되어있다.

Hibernate: create table game_user_role_mapping (id bigint generated by default as identity, game_role_id bigint, game_user_id bigint, primary key (id))
Hibernate: create table game_users (game_user_id bigint generated by default as identity, created_at timestamp, name varchar(20) not null, pw varchar(150) not null, updated_at timestamp, user_id varchar(10) not null, primary key (game_user_id))
Hibernate: create table game_role (game_role_id bigint generated by default as identity, name varchar(255), primary key (game_role_id))
Hibernate: create table users (id bigint generated by default as identity, address1 varchar(10) not null, address2 varchar(50) not null, address3 varchar(50) not null, created_at timestamp, name varchar(20) not null, pw varchar(150) not null, tel varchar(20) not null, user_id varchar(10) not null, primary key (id))
Hibernate: alter table game_users add constraint UK_brcu90ec5s7ntluvp5g0mu4q5 unique (user_id)
Hibernate: alter table users add constraint UK_6efs5vmce86ymf5q7lmvn2uuf unique (user_id)
Hibernate: alter table game_user_role_mapping add constraint FKdavjldo31b9w7w8gixlqkvdsr foreign key (game_role_id) references game_role
Hibernate: alter table game_user_role_mapping add constraint FKc6oeu4jd9rhc3cl8sq6rl7bxt foreign key (game_user_id) references game_users

✅2차 문제

이제 테이블은 수정되었다. 그럼 다음 문제로는 회원가입 부분을 수정해주어야한다. 왜냐면 자동으로 jpa에서 매핑 테이블에 데이터를 넣어줬던 구조에서 이제 내가 넣어줘야한다.

회원가입 부분부터 수정해보자!

@Override
public ResponseGameUser join(RequestGameUser requestGameUser) {
    log.info("user join {}", requestGameUser.getName());
    //유효성 검사
    GameUser user = gameUserRepository.findByUserId(requestGameUser.getUserId());
    if(user != null){
        throw new JoinException(UserCode.EXIST_USER);
    }

    GameUser gameUser = GameUser.builder()
            .userId(requestGameUser.getUserId())
            .pw(passwordEncoder.encode(requestGameUser.getPw()))
            .name(requestGameUser.getName())
            .build();

    GameUser save = gameUserRepository.save(gameUser);
    addRoleToUser(save.getUserId(), "USER");
    ResponseGameUser responseGameUser = new ResponseGameUser(save.getUserId());

    return responseGameUser;
}

해당 부분은 수정할 부분이 없었다. 바로 넘어가자

@Override
public void addRoleToUser(String userId, String roleName) {
    log.info("user add user = {}, role = {}",userId, roleName);

    GameUser user = gameUserRepository.findByUserId(userId);
    GameRole role = gameRoleRepository.findByName(roleName);
    GameUserRoleMapping mapping = GameUserRoleMapping.builder()
            .gameUser(user)
            .gameRole(role)
            .build();
    user.getGameUserRoleMapping().add(mapping);
    gameUserRoleMappingRepository.save(mapping);
}

수정해야할 부분은 권한이 추가되는 부분이였다. 기존의 그냥 role만 add해줬던 부분을 maping으로 넣어줘야하고 mapping repotiory에는 새로운 매핑을 save해주어야한다. 일이 더 늘어난거 같지만... 테이블 자체를 엔티티로 모두 내가 컨트롤할 수 있는게 더 좋은거 같다...

하지만 여기서 조금 거슬리는 부분이 entity가 변경되는 부분이 소스에 그대로 노출되는 것이다. 해당 부분을 domain 내부에서 처리하는 코드로 변경해서 사용해보자.

@Entity
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Table(name = "game_users")
@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "game_user_id")
public class GameUser {
    
    ...
    
    public void addRole(GameUserRoleMapping roleMapping){
        this.gameUserRoleMapping.add(roleMapping);
    }
}

addRole이라는 메서드로 빼서 추가부분을 만들어두고

@Override
public void addRoleToUser(String userId, String roleName) {
    log.info("user add user = {}, role = {}",userId, roleName);

    GameUser user = gameUserRepository.findByUserId(userId);
    GameRole role = gameRoleRepository.findByName(roleName);
    GameUserRoleMapping mapping = GameUserRoleMapping.builder()
            .gameUser(user)
            .gameRole(role)
            .build();
    user.addRole(mapping);
    gameUserRoleMappingRepository.save(mapping);
}

코드도 다음처럼 도메인 자체에 메서드를 사용하는 것으로 변경했다.

동일하게 잘 적용되어지는 것을 확인할 수 있다.

✍️테스트 코드 작성

service 통합 테스트

@SpringBootTest
class GameUserServiceImplInteTest {
    @Autowired
    private GameUserService gameUserService;

    @Test
    @DisplayName("회원가입 테스트")
    void join() {
        //given
        String joinUser = UUID.randomUUID().toString().substring(0,10);
        RequestGameUser user = new RequestGameUser(joinUser, "test1234", "test1234", "테스터");
        //when
        ResponseGameUser join = gameUserService.join(user);
        //then
        assertThat(join.getUserId()).isEqualTo(joinUser);
    }
}

service에서 잘 작동하는지 통합테스트로 먼저 작성했다.

정상처리 되었음을 확인했다.

그런데 여기서 문제는 만약 실제 서버에서 테스트 코드가 실행되어질 때 실제 db에 insert가 일어나는건 싫었다. 당연히 db를 분리해야했고 db를 msa 환경에서 분리는 처음이라 애를 먹었다.

@SpringBootTest(properties = "spring.cloud.config.enabled=false")
class GameUserServiceImplInteTest {
    @Autowired
    private GameUserService gameUserService;

    @Test
    @DisplayName("회원가입 테스트")
    void join() {
        //given
        String joinUser = UUID.randomUUID().toString().substring(0,10);
        RequestGameUser user = new RequestGameUser(joinUser, "test1234", "test1234", "테스터");
        //when
        ResponseGameUser join = gameUserService.join(user);
        //then
        assertThat(join.getUserId()).isEqualTo(joinUser);
    }
}

먼저 SpringBootTest에 properties 옵션을 통해 config server를 실행하지 않도록 수정해주고

server:
  port: 0 #랜덤으로 포트 설정

spring:
  h2:
    console:
      enabled: true
      settings:
        web-allow-others: true
      path: /h2-console
  jpa:
    hibernate:
      ddl-auto: update
    show-sql: true
  datasource:
    driver-class-name: org.h2.Driver
    url: jdbc:h2:mem:test-game-db
    username: sa
    password:
  application:
    name: login-service  #Eureka에 등록되는 서비스 이름

eureka:
  instance:
    instance-id: ${spring.application.name}:${spring.application.instance_id:${random.value}}  #포트가 중복으로 설정되어 구분하기 위한 인스턴스 아이디 값 설정
  client:
    register-with-eureka: true
    fetch-registry: true
    service-url:
      defaultZone: http://127.0.0.1:8761/eureka

logging:
  level:
    com.juno.loginservice.controller: debug
    com.juno.loginservice.service: debug

test > resources > application.yml 파일을 생성하여 테스트용 db 설정을 추가해주었다.

이렇게 실행하면 테스트는 실제 db를 설정값에 설정된 boot 내장 h2 db에서 실행되고 해당 db는 boot 실행이 종료되면 같이 사라진다. 이제 테스트를 맘껏 해도 된다. 그리고 혹시나 위에처럼 결과값을 직접 보면서 하고 싶다면 yml 파일만 수정해주면 끝난다!

service 단위 테스트

@ExtendWith(MockitoExtension.class)
class GameUserServiceImplUnitTest {
    @InjectMocks
    private GameUserServiceImpl gameUserService;

    @Mock
    private BCryptPasswordEncoder passwordEncoder;
    @Mock
    private GameUserRepository gameUserRepository;
    @Mock
    private GameRoleRepository gameRoleRepository;
    @Mock
    private GameUserRoleMappingRepository gameUserRoleMappingRepository;
    @Mock
    private Environment environment;

    @Test
    @DisplayName("회원가입 테스트")
    void join() {
        //given
        RequestGameUser requestGameUser = new RequestGameUser("joinUser", "test1234", "test1234", "테스터");
        String encodePw = "암호화된 비밀번호";
        LocalDateTime date = LocalDateTime.now();
        GameUser gameUser = GameUser.builder()
                .gameUserId(1L)
                .userId(requestGameUser.getUserId())
                .name(requestGameUser.getName())
                .pw(encodePw)
                .updatedAt(date)
                .createdAt(date)
                .build();
        GameRole gameRole = GameRole.builder()
                .gameRoleId(1L)
                .name("USER")
                .build();

        given(passwordEncoder.encode(requestGameUser.getPw())).willReturn(encodePw);
        //when
        when(gameRoleRepository.findByName("USER")).thenReturn(gameRole);
        when(gameUserRepository.save(any())).thenReturn(gameUser);

        ResponseGameUser responseGameUser = gameUserService.join(requestGameUser);
        //then
        verify(gameUserRepository, times(1)).save(any());
        
        Assertions.assertThat(responseGameUser.getUserId()).isEqualTo(responseGameUser.getUserId());
    }
}

처음 작성해본 Mockito를 사용한 단위 테스트여서 많이 헤맸다... 그리고 이렇게 작성하는게 맞는지도 모르겠다. 하지만 우선 기능적으로는 잘 작동하고 테스트 결과까지 잘 반환하므로 다음과 같이 작성해주었다.

가장 헤맸던 부분이 gameUserRepository.save(any()) 이 부분이였는데 any()로 처리하기 전 gameUser 객체를 그대로 넣어줬었는데 여기서 문제가 되는게 테스트 코드에서 들어가는 객체의 해시와 실제 실행되는 코드에서 주입되는 객체의 해시가 다르기 때문에 에러가 발생했는데 그래서 테스트 코드 자체에서 객체를 주입하는 것 말고 any()라는 메서드를 통해 mock에게 어떤 값이 들어가든 gameUser 객체를 반환하라고 적어둔 것이다.

밸덩 참고

profile
코딩을 깔끔하게 하고 싶어하는 초보 개발자 (편하게 글을 쓰기위해 반말체를 사용하고 있습니다! 양해 부탁드려요!) 현재 KakaoVX 근무중입니다!

0개의 댓글