인스턴스의 데이터를 활용해 새로운 상태를 가지는 동일한 인스턴스 생성

2013-09-28 18:11

프로그래밍을 하다보면 인스턴스가 이미 가지고 있는 특정 데이터를 활용해 인스턴스의 상태를 변경하는 경우가 종종 있습니다. 인스턴스의 상태를 변경할 때 여러가지 방식으로 구현하는 것을 종종 확인할 수 있다. 예를 들어 다음과 같은 요구사항이 있다고 가정하자.

  • 텍스트에 url이 포함되어 있는지 확인한다.
  • url이 포함되어 있다면 url에 포함되어 있는 이미지를 찾아 썸네일, 제목, 내용을 추출해 온다.

일반적으로 사용자가 입력하는 데이터와 도메인 모델 클래스가 분리되어 있을 경우 명확하게 역할히 분리되는데 중복을 제거하기 위해 둘의 역할을 한 클래스에서 같이 사용하다보니 이 같은 상황이 종종 발생한다. 위 요구사항에 대한 구현코드는 대략적으로 다음과 같다.

public class SmallTalk {
    private String talk;


    private SiteSummary siteSummary;


    [...]


    public String getTalk() {
        return talk;
    }


    public void setTalk(String talk) {
        this.talk = talk;
    }


    public String getTime() {
        if (TimeUtils.diffDay(new Date(), this.createdDate) == 0) {
            return TimeUtils.agoTime(this.createdDate);
        }
        return "아주 오래전...";
    }


    public SiteSummary getSiteSummary() {
        return siteSummary;
    }


    public void setSiteSummary(SiteSummary siteSummary) {
        this.siteSummary = siteSummary;
    }
}

위와 같이 setter와 getter를 가지는 클래스를 생성한다. 위 요구사항을 만족하려면 talk 데이터를 활용해 SiteSummary를 생성하기 위해 필요한 데이터를 생성해야 한다. SiteSummary는 url에 대한 썸네일 정보, 제목, 내용을 포함하는 클래스이다.

public class SiteSummary implements Serializable {


    private static final long serialVersionUID = -7183093514202968034L;


    private String title;
    private String contents;
    private String thumbnailImage;
    private String targetUrl;


    public SiteSummary(String title, String contents, String thumbnailImage, String targetUrl) {
        this.title = title;
        this.contents = contents;
        this.thumbnailImage = thumbnailImage;
        this.setTargetUrl(targetUrl);
    }


    public SiteSummary(Document doc, String targetUrl) {
        this.title = doc.title();
        this.thumbnailImage = SiteImage.getImage(targetUrl, doc);
        this.contents = SiteContents.getContents(doc);
        this.setTargetUrl(targetUrl);
    }


    [...]
}

이와 같이 한 인스턴스의 특정 데이터를 활용해 이 인스턴스의 상태를 변경해야 되는 요구사항은 자주 발생한다. 이 요구사항은 다음과 같이 구현할 수 있다.

public class SmallTalkService { 
    public List<SmallTalk> getLastTalks() {
        Page<SmallTalk> page = smallTalkRepository.findAll(getPager());
        List<SmallTalk> orgSmallTalks = page.getContent();
        
        List<SmallTalk> smallTalks = Lists.newArrayList();
        for (SmallTalk smallTalk : orgSmallTalks) {
            smallTalk.setSiteSummary(summaryService.findOneThumbnail(smallTalk.getUrlInTalk()));
            smallTalks.add(smallTalk);
        }
        return smallTalks;
    }
}
public class SummaryService {
    public SiteSummary findOneThumbnail(String url) {
        [...]
    }
}

위 소스 코드를 보면 SmallTalk에서 url을 추출해서 summaryService에 역할을 위임해 SiteSummary를 생성한 SmallTalk 클래스의 setSiteSummary() 메소드를 활용해 인스턴스의 상태를 변경하고 있다.

내가 만나본 대부분의 개발자는 위와 같이 구현하는 경우가 일반적이다. 하지만 뭔가 찜찜하다. 위 방식 말고 다른 방식으로 개선한다면 어떻게 개선할 수 있을까?

PS. 이 글에 대한 요구사항은 메인 화면의 수다양 구현 중 발생한 소스 코드의 일부분이다. 요구사항은 수다양 기능을 통해 확인할 수 있다.

0개의 의견 from FB

8개의 의견 from SLiPP

2013-09-28 18:30

@eclipse4j 뭐 그 정도는 기본 아니야. 사실은 댓글 기능에 대해서는 캐시 걸지 않았거든. 약간 느리긴 해도 실시간으로 확인하는 것이 의미가 있겠다고 생각해서...

네 생각은 어떠냐? 실시간으로 가도 괜찮겠냐?

2013-09-28 18:34

혹시 페북 그룹가입도 API 제공 되는 건가요?

from 쪽이 좀 더 부각되면 좋겠는데요. 그리고 댓글이 많아지면 좀 어지럽다는.. 폴딩기능도 추가 해야 할뜻..

이전 댓글 몇개 보기 등..

2013-09-28 18:38

@eclipse4j 페북 그룹 가입 api는 찾아봐야겠다.

그룹에 대한 이슈는 https://github.com/javajigi/slipp/issues/144 에 진우가 올려놨다. 여기 댓글에다 요구사항 추가해 주면 진우가 개선할 듯하다. 내가 봐도 그룹명이 좀 작다는 생각이 든다.

댓글이 많아질 경우에 대한 개선은 좀 더 추이를 살펴보고 댓글이 많은 글들이 보이면 그 때 개선점을 찾아보자. 이 논의는 이 글의 답변을 위해서 여기까지 하고 필요하다면 다른 곳에서 하자.

2013-09-29 09:15

@eungju.park.1 좋은 접근 방식이고 가능하다고 생각한다. URL 데이터를 읽어서 URL 페이지 정보를 추출하는 부분이 SmallTalk에서 구현하면 충분할 듯하다.

그렇다면 이런 경우는 어떨까? 현재 SummaryService에서 SiteSummary를 생성하는데 비용발생이 큰 관계로 Cache 처리를 다음과 같이 하고 있다.

@Service
public class SummaryService {
    @Cacheable(value="smallTalkCache", key="#url")
    public SiteSummary findOneThumbnail(String url) {
        [...]
    }
}

Cache 처리를 하려면 SummaryService와 같이 Spring이 관리하는 별도의 클래스가 반드시 필요한 상황이다. 하지만 로직은 SmallTalk에서 처리하고 싶을 경우 다음과 같이 구현하면 어떨까?

public class SmallTalk {
    [...]


    public SmallTalk processUrlInTalk(SummaryService summaryService) {
        SiteSummary siteSummary = summaryService.findOneThumbnail(this.getUrlInTalk());
        return new SmallTalk(this.smallTalkId, this.talk, siteSummary);
    }
}

약간의 비용 발생이 있을 수 있겠지만 새로운 상태를 가지는 SmallTalk을 새로 생성하는 방법은 어떨까? 현재 DTO와 도메인 모델 클래스가 클래스 하나에서 관리되고 있기 때문에 발생하는 이슈인데 이 둘을 상태에 따라 다른 인스턴스를 생성하도록 구현하는 것이 더 낫지 않을까?

2013-09-29 11:35

@자바지기 모델에 서비스를 넘기는 형태의 구현으로 필요한 상태의 모델을 만드는 방법을 저도 사용해봤는데 유지보수할때 사람들이 혼란스러워 하더라구요. 이정도 고민이 시작되면 저는 도메인모델과 DTO를 나눕니다. 이런 케이스가 많지는 않아서 DTO관리하는 것은 비용이 크지 않다고 봅니다.

2013-09-30 11:13

레이어간 데이터교환을 위한 오브젝트가 도메인 오브젝트에 포함되는 순간 애매한 문제들이 너무 많은 거 같아요. 저 같으면 처음 제시한 방법을 대체로 따를 것 같아요. 만약 SiteSummary가 항상 필요한 것이 아니라면 클라이언트에게 책임을 전가하는 것도 방법이라는 생각이 들더라구요.

Page<Smalltalk> page = smalltalkService.getRecentTalks();
siteSummarySummaryService.findSiteSummariesBy(page);

SiteSummary가 항상 필요하다면 smalltalkService에서 모든 것을 만들어서 내놓는 것이 좋아보이네요.

그리고 Smalltalk에게 Service 객체를 넘기게 되면, 오브젝트에 대한 의존성도 커지게 되고 ...서로서로를 알게되는 상황이라 복잡도가 높은 것 같아요.

의견 추가하기