본문 바로가기

개발 프로젝트/Zerobase 연습프로젝트 기록

매장 테이블 예약 서비스(trouble) 문제발생 및 해결

문제1. 데이터 초기값 오류 

 

- Resource Pkg 상의 sql.data을 통해 초기값을 입력하여 테스트를 진행하고자 하였으나 <JdbcSQLIntegrityConstraintViolationException: NULL not allowed for column "ID"> Error 발생

-  Entity를 AutoGenerate로 설정하였기 때문에 SQL문과 Entity 상에 표면적으로는 문제가 없어보였음

- H2 DB의 Table INFORMATION_SCHEMA.COLUMNS을 확인하여 IS.IDENTITY, IDENTITY.GENERATION이 no와 Null값으로 표기된 것을 확인함.

- API를 통해서 데이터를 삽입시 JPA Hibernate:  insert  into partner_entity (business_id, partner_id, partner_name, registered_at, id)  values (?, ?, ?, ?, ?)를 확인하여 ID가 Database가 아닌 JPA 측에서 AutoIncrement하는 것을 확인함.

Entity구조

public class PartnerEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    @Column(unique = true)
    private String partnerId;
    private String partnerName;
    private Long businessId;
    private LocalDateTime registeredAt;


}

 

data.sql 초기값

INSERT INTO partner_entity (partner_id, partner_name, business_id, registered_at)
VALUES ('P001', 'Partner One', 1234567890, '2024-01-01 10:00:00');

INSERT INTO partner_entity (partner_id, partner_name, business_id, registered_at)
VALUES ('P002', 'Partner Two', 1002, '2024-01-02 11:00:00');

INSERT INTO partner_entity (partner_id, partner_name, business_id, registered_at)
VALUES ('P003', 'Partner Three', 1003, '2024-01-03 12:00:00');

INSERT INTO partner_entity (partner_id, partner_name, business_id, registered_at)
VALUES ('P004', 'Partner Four', 1004, '2024-01-04 13:00:00');

INSERT INTO partner_entity (partner_id, partner_name, business_id, registered_at)
VALUES ('P005', 'Partner Five', 1005, '2024-01-05 14:00:00');

 

- sql문에 id를 직접 입력함으로써 문제 해결함

INSERT INTO partner_entity (id, partner_id, partner_name, business_id, registered_at)
VALUES (1, 'P001', 'Partner One', 1234567890, '2023-01-01 10:00:00');

INSERT INTO partner_entity (id, partner_id, partner_name, business_id, registered_at)
VALUES (2, 'P002', 'Partner Two', 2345678901, '2023-01-02 11:00:00');

INSERT INTO partner_entity (id, partner_id, partner_name, business_id, registered_at)
VALUES (3, 'P003', 'Partner Three', 3456789012, '2023-01-03 12:00:00');

INSERT INTO partner_entity (id, partner_id, partner_name, business_id, registered_at)
VALUES (4, 'P004', 'Partner Four', 4567890123, '2023-01-04 13:00:00');

INSERT INTO partner_entity (id, partner_id, partner_name, business_id, registered_at)
VALUES (5, 'P005', 'Partner Five', 5678901234, '2023-01-05 14:00:00');

 

문제2. 보안기능 추가를 위한 Entity 구조 변경 

 

방안2.1 최초구상 -PartnerEntity와 UserEntity를 하나로 병합

  처음 설계시 보안(인증과 권한)기능을 부여하지 않을 것으로 생각하고 Partner과 User의 Table을 별도로 두었고 Credential에 대한 칼럼도 포함하지 않았음. Partner나 User의 로그인시 두개의 Table을 각각 탐색하는 것은 비효율적이므로 하나의 Table로 두고서 Credential을 저장하고, 그외 DAO Authentication, JWT Authentication, Security Filter도 구현할 것.

 

그 외 고려사항

- CRUD 관계를 고려하여 Table관계 재정리

- Business Flow를 다시 고려하여 명세서 재작성하기

 

객체지향적으로 작성해서 수정점이 많이 없을지도 모름

 

방안2.2 방향변경 - PartnerEntity와 UserEntity를 변경하지 않고 제 3의 MemberEntity를 추가

 

  두개의 테이블을 합치려고했으나 Entity가 Repositry, Dto, ServiceLayer 그리고 Controller에 들어가는 request Dto까지 침투되어 있어서  많은 코드 변경이 필요했고 기존 테이블을 제거하고 새로운 테이블을 생성하는 변경을 하기보다는,  로그인 기능을 위해서 하나의 별도의 MemberEntity를 추가하고 기존 테이블간의 관계를 형성하는 방향으로 결정하였음.

 

기능추가시 깨달은 점

 -  설계단계에 존재하지 않았던 기능을 추가할때는 Open-Close원칙이 왜 중요한지 알게되었음. 

- Backend와 Frontend(디자인)의 협업이 왜 중요한지 알게되었음 ; 완전한 구조로 설계가 되면 좋겠지만 기능변경이 생길수록 정리되지 않은 기능과 코드들이 생기고, 이를 프론트 측과 협의하여 서비스로 구현해야할 필요가 생긴다.

- 코드가 조금 중복되고 추가되더라도 가독성이 좋도록 Class분리를 하는 게 좋다

 

기능 및 클래스 추가시 깨달은점

- Entity가 같이 연동되어야하므로 같은 @Transactional에 같이 넣을것 

 

 

깨달은점

A. 최초 기획단계에서 기능 중심으로 정확하게 기획해야 불필요한 기능들을 추가하지 않게된다.

B. 모든 기능 요구사항은 최초 테이블/API 설계에 큰 영향을 준다. 

C. 기능이 추가된다면 기존 코드를 수정하는 방안보다는 클래스를 추가하는 Open/Close 방식을 고려해라

 

문제3. Circular Dependency 문제발생으로 인한 클래스 디자인검토

  Repository,Entity,Service,Controller만 사용하여 소위 MVC패턴을 그대로 따라할 때에는 발생하지 않았던 Circular Dependency문제가 발생했다. Spring Security기능을 위해서 JwtFilter, JwtToken, UserDetailsService, SecurityConfig를 구현하다보니 MVC패턴에 적용되지 않는 구조였고 SRP원칙이 잘지켜지지 않았던 것으로 추정된다.

 

 

 

 

3.1 디자인 검토사항

 

Circular Dependency관련해서 잠재적 원인을 찾아보니 아래의 두가지 디자인원칙을 잘지키지 못해서 Circular Dependeny가 발생한 것이 아닌가?라는 질문이 있었고 해당 관점에서 현재 디자인을 검토함. CleanArchitecture에 대해서 좀 더 깊게 학습하고 Refactoring을 진행하도록 함. 

 

3.2 사용 디자인 관점

A. SRP(Single Responsiblity Principle) : 단일책임

B. Clean Architecture Principles : the key principle is that dependencies should always point inward 

 

3.3 클래스 리팩토링

A. MemberAuthService  : daoAuth메소드 제거 후, 본 로직을 Controller로 하향이전, JwtHandler로의 의존성 제거

- MemberAuthService는 SRP관점에서 memberDetail을 다루는 본 클래스이나 daoAuth()는 token발행 메소드로 본 클래스에서 분리되어야함.

- daoAuth()는 Method의 역할을 보았을 때도 (1) ID,PW를 검증하는 작업 (2) Token을 발행하는 작업이 하나의 메소드로 처리되어 있어 분리의 필요성이 있다.

- jwtHandler인 인터페이스 어뎁터를 호출하는 역할은 Service Layer(순수한 어플리케이션 본연의 로직)과 관련성이 낮으므로 Clean Architecture Principles에 따라, 해당 역할은 Controller Layer로 내려가는게 맞음

public String daoAuth(String memberId, String password){

    MemberDetails memberDetails = memberRepository.findByMemberId(memberId).
            orElseThrow(() -> new UsernameNotFoundException("Member does not exist"));

    if(!passwordEncoder.matches(password, memberDetails.getPassword())){
        throw new CustomException(ErrorCode.PASSWORD_UNMATCHED);
    }

    return jwtHandler.generateToken(memberDetails.getMemberId(),
            memberDetails.getRole());
}



B. JwtHandler : getjwtAuthentication() filter클래스로 이동후 memberAuthservice와 의존성제거

- SRP관점에서 jwt라이브러리의 인터페이스 어뎁터에서 jwt라이브러리와 무관한 Authentication생성을 할 필요가 없음. Authentication과 좀 더 관련이 있는 filter class로 이동

public Authentication getJwtAuthentication(String jwt) {
    log.info("creat authentication through token : " + jwt);
    UserDetails userDetails =
            memberAuthService.loadUserByUsername(this.getUsernameFromToken(jwt));
    return new UsernamePasswordAuthenticationToken(userDetails,"",
            userDetails.getAuthorities());
}

 

C. SecurityConfig : passwordEncoder를 별도의 appconfig class로 이전

PasswordEncoder가 ServiceLayer에 사용되면서 Circular dependency를 유발하여, 별도의 Appconfig에서 passwordEncoder을 정의함. 

@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}