두 번째 프로젝트의 코드 작성이 거의 끝났고, 테스트 코드 작성을 앞두며 코드 리뷰를 받았다.
가장 큰 골자는 아무래도 참조 관계이다.
패키지 구조를 Layered에서 약간의 DDD(애매하지만 ㅎㅎ) 를 곁들인 구조로 변경했다.
그 과정에서 패키지 간 의존성에 대해서 고민해보고 작명하는 것과 설계하는 시간이 정말 오래 걸렸다.
코딩을 공부하면 할 수록 작은 것에 시간을 오래 들이게 되는 걸 느낀다.
어제는 패키지 이름을 짓는데 반나절이 걸렸다.
회사에서는 변수명 짓는 걸로도 회의를 한다고 하니 약간 실감이 나기도 한다.
이렇게 디테일하게 채워나가면 그 만큼 내 실력이 된다고 믿습니다.
최상위 구조
- auth : 인증, 인가 처리
- 스프링 시큐리티 이용
- 스프링 컨테이너까지 도달하지 않는 필터 위주이기 때문에 패키지를 따로 구성했다.
- domain
- 도메인 관련 서비스로 구성된 패키지
- global
- 모든 패키지에서 사용 가능
- 전역적으로 적용
- usecase
- 도메인의 비즈니스로직에서 패키지 간 의존성을 따로 분리하기 위해 구성한 패키지
- 주문, 결제에서 수 많은 참조를 가지기 때문에 분리해서 usecase로 추가했다.
차상위 구조
디테일하게 설명하진 않겠지만, 추상적인 네이밍보다는 의미를 가진 이름으로 최대한 지어보려고 했다.
Redis
Redis 관련된 설정과 클래스들의 구조를 고민하고 개편했다.
RedisFunction
에서는 트랜잭션의 책임까지 갖고 있는 RedisFunctionProvider 객체였다.
트랜잭션 처리는 최대한 스프링이 제공해주는 @Transactional을 이용하는 것이 바람직하다고 의견을 들었기 때문에, 따로 트랜잭션 처리 Aspect를 과감히 삭제했다.
public interface RedissonFunctionProvider {
RLock lock(String lockKey);
RLock lock(String lockKey, long timeout);
RLock lock(String lockKey, TimeUnit unit, long timeout);
boolean tryLock(String lockKey, TimeUnit unit, long waitTime, long leaseTime);
void unlock(String lockKey);
void unlock(RLock lock);
Object getValue(String key);
void setValue(String key, Object value);
boolean canUnlock(String lockKey);
}
- 트랜잭션 메서드를 삭제
@Documented
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RedisLocked {
/**
* 사용자 정의 redisson lock Key
*/
String key() default "";
/**
* redisson lock 유지 시간 <br>
* 단위 : milliseconds
*/
long leaseTime() default 3000;
/**
* redisson lock 획득 대기 시간 <br>
* 단위 : milliseconds
*/
long waitTime() default 3000;
}
- 파라미터에 종속적이지 않기 위해 락 키를 손수 받아오는 설정을 추가
@Aspect
@Component
@RequiredArgsConstructor
@Slf4j
@Order(value = Integer.MIN_VALUE)
public class RedisLockAspect {
- @Transactional로 먼저 감싸지고 최후에 이 Aspect가 감싸지는 것을 원하므로 @Order를 조정했다.
Redis Store
RedisStore 객체는 추상화된 RedisTemplate을 이용하여 Redis에 접근해 데이터를 넣고 빼고 하는 역할을 했다.
하지만 필드는 StringRedisTemplate만을 박아두었었고, Global 패키지에 구성했기 때문에 다른 RedisTemplate을 사용하는데 한계가 있고, 전역적 사용의 어려움과 SOLID 2원칙에 위배되었다는 사실을 알게 되었다.
그래서 추상클래스로 한번 정의 해봤다.
@RequiredArgsConstructor
public abstract class RedisStore {
private final RedisTemplate<String, Object> redisTemplate;
private final ObjectMapper objectMapper;
public <T> T getData(String key, Class<T> valueType) {
ValueOperations<String, Object> valueOperations = redisTemplate.opsForValue();
String value = (String) valueOperations.get(key);
if (StringUtils.isBlank(value)) {
throw new BadRequestException("해당 키에 맞는 값이 존재하지 않습니다.");
}
try {
return objectMapper.readValue(value, valueType);
} catch (JsonProcessingException e) {
throw new InternalErrorException(e);
}
}
public void setData(String key, Object value) {
try {
String jsonValue = objectMapper.writeValueAsString(value);
ValueOperations<String, Object> valueOperations = redisTemplate.opsForValue();
valueOperations.set(key, jsonValue);
} catch (JsonProcessingException e) {
throw new InternalErrorException(e);
}
}
public void setDataExpire(String key, Object value, long duration) {
try {
String jsonValue = objectMapper.writeValueAsString(value);
ValueOperations<String, Object> valueOperations = redisTemplate.opsForValue();
Duration expireDuration = Duration.ofSeconds(duration);
valueOperations.set(key, jsonValue, expireDuration);
} catch (JsonProcessingException e) {
throw new InternalErrorException(e);
}
}
public Boolean deleteData(String key) {
return redisTemplate.delete(key);
}
}
- 일단 가장 많이 사용하는 String, Object로 구성을 했다.
- 제네릭과 Object mapper를 사용해서 값을 가져올 때, 오브젝트로 가져올 수 있게 짰다.
@Component
public class UserRedisStore extends RedisStore {
public UserRedisStore(RedisTemplate<String, Object> redisTemplate, ObjectMapper objectMapper) {
super(redisTemplate, objectMapper);
}
}
- 사용 클래스는 @Component로 Bean 등록을 하고 필요한 템플릿과 매퍼를 주입받아 상위 클래스에 제공한다.
- 다른 레디스 템플릿을 사용할 경우 직접 주입받아 재정의 하면 될 것 같다.
이게 최선의 구성인지는 잘 모르겠다.
이펙티브 자바를 보면서 상속보다는 컴포지션을 사용하라는 아이템 챕터를 읽었는데,
상속을 구현할 때는 메서드의 재사용보다는, 타입을 위해 사용하라는 말이 있었다.
진정한 Is - A 관계일 경우를 생각해보라는 것이다.
근데 나는 재정의를 안 할 경우 재사용을 하는 것 같기도 하다. 아직까지는 이게 최선인 듯 싶다.
감상
아직까지 부족한 게 많은 것 같다.
완벽하게 채울 순 없다는 생각이 들었다. 주어진 상황에서 최선을 다할 뿐
속도가 느리다고 생각하지 말고, 꼼꼼한 구성에만 집중하자.
이제 테스트 코드를 짜고 있다.
테스트 코드를 짜다보니까 클래스 간 강결합을 지양하라는 의미를 비로소 알게 되었다.
더 작은 객체, 작은 테스트를 위한.
데이터 테스트를 할 때에는 H2 db를 사용해서 구현하는 것도 잊지 말자
'📕 Spring Framework > Spring Project' 카테고리의 다른 글
[리팩토링] 도메인 모델 중심 Clean Architecture 로의 리팩토링 (0) | 2022.12.12 |
---|---|
[이슈] Pageable test 관련 에러 (0) | 2022.11.23 |
[Redisson] 트랜잭션 문제 발생 및 해결 (0) | 2022.10.01 |
[Redisson]을 이용한 분산 Lock 구현 & 동시성 문제 해결 (2) | 2022.09.27 |
결제 API 리팩토링 - [2] (feat. WebClient) (7) | 2022.09.22 |