리팩토링 (불변 객체, 캐시, 객체지향)

yeezze·2022년 12월 14일
0

form 프로젝트

목록 보기
4/7

객체지향적 관점으로 고민하고 유지보수가 쉬운 코드로 리팩토링을 진행해보자!

해당 프로젝트의 로직을 간단히 설명하면 여러개의 이벤트가 동시에 진행될 수 있고 각 이벤트는 선착순 로직을 개별적으로 진행해야한다.
여러명의 유저들은 각 선착순 이벤트가 시작되는 시간에 동시 접속하여 이벤트 참여요청 API를 호출하여 참가할 수 있다.

기존 코드

Event VO 객체

public class Event {

    private static final int END = 0;

    private ObjectId formId;
    private LocalDateTime start;
    private LocalDateTime end;
    private int winnersNum;
    private int limitCount;

	// 내부 생성자 캡슐화
    private Event(ObjectId formId, LocalDateTime start, LocalDateTime end, int winnersNum) {
        this.limitCount = (int) (winnersNum * 0.1);
    }
    
    // 정적 팩토리 메서드
    public static Event createfromForm(Form form) {
        return new Event(form.getId(), form.getStartTime(), form.getEndTime(), form.getWinnersNumber());
    }

Event 객체는 Form을 인자로 받는 정적 팩토리 메서드를 활용한 생성 방식을 활용하고 있다.
기본 생성자는 private로 캡슐화해주고 외부에서 Event 객체를 생성하려면 해당 메서드를 활용하면 된다.

선착순 Serivce class

개별 이벤트를 활용해서 선착순 로직을 관리하는 Service class이다.

public class SubmissionService {
	private final HashSet<Event> eventHashSet = new HashSet<>();
    
    private void addActiveEventList(Event event) {
        eventHashSet.add(event);
    }
    
    // 유저가 선착순 응답 참여 API를 호출하면 실행되는 메소드
    // 다수의 유저가 동일한 event를 참여하면 새로운 event객체를 항상 생성하게됨
    public Long addQueue(String userName, Event event, List<Answer> answerList) {
        addActiveEventList(event);
        ... (생략)
    }
}

해당 클래스 내에 여러개의 이벤트가 동시에 진행될 수 있기에 HashSet으로 여러개의 Event를 service 클래스 내에서 필드로 관리했다.

문제

이렇게 기존 코드로 짜놓고 테스트 코드를 작성하던 중에 고민이 생겼다.
기존 로직대로라면 다수의 유저가 동일한 event를 참여하면서 API를 각각 호출하면 동일한 event에 대해 새로운 event객체를 항상 생성하게된다.
만약 1000명이 참가한다면 똑같은 event 객체가 1000개가 생성되어 버리는 것이다.
불필요한 VO객체가 생기게 되는 문제가 있다.

만약 다수의 유저들이 접속하는 실제 서비스라면?
500명 선착순 이벤트에 10000명이 접속했다면?

이펙티브자바 아이템 6 : 불필요한 객체 생성을 피하라

10000개의 불필요한 이벤트 객체가 생성된다.
1개의 이벤트는 맨처음 한번만 생성되면 된다.
객체 생성 시기는 무조건 1등으로 선착순에 신청한 유저가 참가 API를 호출했을 때이다.
그렇다면

해결

정적 팩토리 메서드 내에서 캐시를 활용하자!

는 결론을 도출했다.

세부 고민

이제 또 고민이 생긴다.

  • 전체 Evnet set은 선착순 로직 Service class 내의 필드로 관리되고 있다.

    • set에 add하는 로직도 Service 내에 addQueue() 메소드 내에서 진행된다.
  • Event의 생성로직은 Event 객체 내에서 관리된다.

    • 여기서 캐시를 활용하려면 Serivce class 내에 있는 필드를 참조해와야한다.
    • 그렇다면 의존관계가 생긴다.
    • Event -> Service를 필드로 의존해야 set을 getter로 가져올 수 있다.

구현했을 때 UML

class Event{

	public static Event createfromForm(Form) {
      HashSet<Event> eventSet = submissionService.getEventHashSet();

      if(eventSet.contain()) {
          // 기존 객체 리턴
      }
	
   		// 객체 새로 생성해서 리턴
	}
}

정적 팩토리 메서드가 이런식으로 캐시 로직을 가지게 된다.

이런식으로 로직을 짜게 되면 문제가 생긴다.

1. Event 객체를 관리하는 주체가 불분명하다.

구현해야하는 포인트는 Event 객체가 생성되고 Set으로 관리되야한다.

그런데 객체 생성은 VO 내에서 진행되고
생성된 객체를 Set에 추가하는 코드는 Service 클래스의 메소드 내에서 진행되는 문제가 발생한다.

public class SubmissionService {

    public Long addQueue(String userName, Event event, List<Answer> answerList) {
        eventHashSet.add(event);	// set 추가
        ... (생략)
    }
}

-> Event 객체에 대한 관리 주체를 하나로 모아야한다.

2. 도메인 레이어의 VO 객체가 Service를 의존해야하고, 불필요한 의존관계가 생성된다.

public SubmissionService(AnswerService answerService, RedisTemplate<String, String> redisTemplate) {
     this.answerService = answerService;
     this.redisTemplate = redisTemplate;
    }

SubmissionService는 다수의 의존관계를 가지고 있다.
이러한 클래스를 Event VO 객체가 의존하려면 엮여있는 의존관계를 모두 끌고 와야한다.
불필요한 의존관계를 가지게하는 것이 좋을까?

어떻게 구현해야 좋을까?

개선점을 고려하여 구현해보자

Event 객체가 생성됨과 동시에 집합 set에도 추가되도록 로직을 변경함으로써
Event 객체의 생성 책임과 응집력을 높여주어야겠다.

package com.~~.vo;

public class Event {

    private ObjectId formId;
    private LocalDateTime start;
    private LocalDateTime end;
    private int winnersNum;
    private int limitCount;

    Event(ObjectId formId, LocalDateTime start, LocalDateTime end, int winnersNum) {
    	this.formId = formId;
        this.start = start;
        this.end = end;
        this.winnersNum = winnersNum;
        this.limitCount = (int) (winnersNum * 0.1);
    }
}
package com.~~.vo;

public class EventFactory {
    
    private static final Map<ObjectId, Event> eventMapCache = new ConcurrentHashMap<>();

    public static Event createfromForm(Form form) {
        ObjectId formId = form.getId();
        if (eventMapCache.containsKey(formId)) {
            return eventHashMap.get(formId);
        }
        Event event = new Event(form.getId(), form.getStartTime(), form.getEndTime(), form.getWinnersNumber());
        eventMapCache.put(formId, event);
        return event;
    }
    
    public Set<Map.Entry<ObjectId, Event>> events() {
        return eventMapCache.entrySet();
    }
    
    public void removeEvent(ObjectId formId) {
        eventMapCache.remove(formId);
    }
}

별도의 EventFactory class를 생성해서 객체 생성, 관리 책임을 따로 빼줬다.

2개의 class를 동일한 패키지 내에 넣어두고 Event 객체의 생성자는 default로 만들어서 외부에서는 Event 객체를 직접 생성할 수 없고, 정적 팩토리 메서드를 통해서만 생성하도록 구현했다.
생성자 캡슐화와 객체의 응집력 상승 모두 갖출 수 있도록 구현했다.

또한 캐시 기능을 넣어서 불필요한 객체 생성을 줄이도록 개선했다.

기존에 Set으로 관리되던 이벤트 집합을 Map(eventMapCache)으로 변경하여 Event객체의 Key를 통해 조회할 수 있도록 했다.
Map(eventMapCache) 필드는 불변객체로 사용되어야한다.
해당 불변 객체를 SubmissionService에서 활용할 수 있다. (조회, 삭제)

불변객체 구현

불변객체 구현에서 발생할 수 있는 문제

만약 public static final 필드로 map을 선언하고 get() 메소드로 해당 map을 외부에 넘겨주게 된다면...
final로 선언했다고 불변객체인 줄 알겠지만
불변객체가 아니게 된다!!!!!

외부에서 new map을 해버리면 새로운 map 객체가 할당되게 된다.

문제가 발생하지 않도록 구현..

불변객체로 활용되기 위해서 private static 필드로 선언해주고 활용 메소드를 별도로 만들었다.
클라이언트(SubmissionService)에서는 메소드를 통해 사용하면된다.

조회(get)는 원본 데이터의 수정은 없이 데이터를 활용할 수 있게 Stream으로 제공해주는 메소드를 생성했다.
삭제는 별도의 메소드를 만들었다.

SubmissionService에서 eventMapCache을 어떻게 활용하는지는 다른 게시물에서 다루어보겠다!
투비컨티뉴..~

profile
백엔드 개발자 😊

0개의 댓글