slipp 사이트는 태그를 기반으로 동작하도록 구현되어 있다. slipp 사이트의 태그 기반 소스 코드를 점진적으로 같이 구현해 보면 재미있을 듯해서 요구사항 및 코드의 일부를 공개한다.
slipp의 태그 요구사항은 다음과 같다.
- 태그는 공백 또는 쉼표로 구분한다.
- 태그는 우측에 보이는 바와 같이 태그 풀이 이미 존재한다.
- 글을 쓸 때 태그 풀에 존재하는 태그를 사용하면 태그의 사용 수가 1 증가한다.
- 태그 풀에 존재하지 않는 태그를 사용할 경우 신규 태그로 등록되어 별도로 관리된다.
먼저 태그 풀에 존재하는 태그를 관리하는 Tag 클래스이다.
package net.slipp.tag;
public class Tag {
private String name;
private int taggedCount;
public Tag(String name) {
this.name = name;
}
public String getName() {
return name;
}
public int getTaggedCount() {
return taggedCount;
}
}
신규 태그에 대한 상태 정보를 담고 있는 NewTag 클래스이다.
package net.slipp.tag;
public class NewTag {
private String name;
public NewTag(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
태그 풀에서 태그 이름으로 태그를 조회하는 TagRepository 클래스이다.
package net.slipp.tag;
public interface TagRepository {
Tag findByName(String name);
}
마지막으로 태그를 태그 풀 태그와 신규 태그로 분리하는 작업을 담당하는 TagProcessor 클래스이다.
package net.slipp.tag;
import java.util.Set;
import com.google.common.collect.Sets;
public class TagProcessor {
private TagRepository tagRepository;
private Set<Tag> tags = Sets.newHashSet();
private Set<NewTag> newTags = Sets.newHashSet();
public TagProcessor(TagRepository tagRepository) {
this.tagRepository = tagRepository;
}
public void processTags(String plainTags) {
// TODO 태그 풀에 존재하는 태그와 신규 태그를 신규 태그를 분리한다.
}
public void processTags(Set<Tag> originalTags, String plainTags) {
// TODO 태그 풀에 존재하는 태그와 신규 태그를 신규 태그를 분리한다.
}
public Set<Tag> getTags() {
return tags;
}
public Set<NewTag> getNewTags() {
return newTags;
}
}
TagProcessor에서 processTags(String plainTags) 메소드는 최초로 글을 쓸 때 사용하는 메소드이다. 즉, 태그 풀에 존재하는 태그와 신규 태그만 분리해서 각각의 Set Collection에 추가해 주면 된다.
processTags(Set
- java,javascript,newTag 태그로 글을 쓴다. java와 javascript는 태그 풀에 존재하기 때문에 태그 풀 Set에 추가하고 태그 사용 수는 1 증가시킨다. newTag는 신규 태그 Set에 추가한다.
- 글을 수정할 때 java,web,newTag2를 태그로 사용한다. java,web은 태그 풀에 존재한다면 태그 풀 Set에 java,web이 추가되고, 신규 태그 Set에는 newTag2가 추가되어야 한다. 그런데 여기서 복잡도가 증가하는 부분은 태그 사용 수를 조정하는 부분이다. java는 생성시 이미 사용 수가 1 증가했기 때문에 수정 시에는 증가하지 않아야 한다. web은 신규로 추가되는 태그이기 때문에 +1, javascript는 수정시 제외된 태그이기 때문에 -1이 되어야 한다.
수정할 때는 생각만 해도 요구사항을 만족시키기 쉽지 않다. 위와 같은 요구사항을 만족하려면 위 소스 코드는 어떻게 구현하면 될까? 이에 대한 단위 테스트 코드는 어떻게 구현할까?
24개의 의견 from SLiPP
기본적인 질문인데.. Tag와 NewTag를 따로 구분해야될 이유가 어떤게 있을까?
추가로 Tag를 실시간으로 하기보단 Async하게 처리하는건 어떨까? 생각 한다. 일단 간단히 입질만 해봄..
Tag와 NewTag를 분리하지 않고 하나로 합쳐도 된다. 나는 두 개를 분리하는 것이 데이터 분리도 편하고 향후 확장하는데 좋지 않을까 생각해서 분리해 봤다. 구현하다보니 비슷한 부분이 많은 건 사실이다.
async로 구현하는 건 일단 로직을 구현한 이후의 고민일 듯 싶다. 일단 로직 구현한 후에. ㅋㅋ
수정시의 요구사항을 생각해보니 차집합을 이용하면 될 것 같네요.
originalTags 집합 - plainTags 집합 = -1 씩 카운트 plainTags 집합 - originalTags 집합 = +1 씩 카운트
교집합은 자연스레 0 이 될 듯합니다.
@자바지기
메모장에서 걍 작성한 pseudo code 다.
굳이 TagProcessor class없이 잘될거 같은데.... Tag나 NewTag모두 동일한 도메인이 아닌가? 만일 구분을 해야될 필요가 있다면 tag.count 를 조건으로 둘을 filter mapping하면 될듯 한데??
데이터의 분리도 count의 조건으로 하면 문제 없을듯 하고 확장을 위해서라면 그러한 필요성이 있을때 하는게 좋지 않을까? Tag를 interface로 해도되고...
내가 보기엔 Tag와 NewTag를 쓰기위해 너무 큰 overhead가 있는듯 하다.
@jhindhal.jhang 내가 처음 개발을 시작할 때는 태그 풀에 있는 태그만 사용할 수 있도록 정의했었다. 그래서 처음에 Tag가 등장했다가 너무 제약이 많은 듯해서 신규 태그를 사용할 수 있도록 허용했다. 신규 태그는 태그 풀에 등록할 후보로 사용하고 이후에 태그 풀로 이동할 수 있다는 생각을 했다. 나도 처음 신규 태그 기능 추가할 때 Tag 하나로 갈까 NewTag로 분리할까 고민하다가 일단은 분리하는 것으로 결정했다. 한 동안 운영해 보고 네 의견 들어보니 Tag와 NewTag를 지금 시점에 나누는 것은 분명 overhead라는 생각이 든다. 이런 관점에서 코드를 다음과 같이 구현해 봤다. 정상 동작하지는 않을 수 있다.
Tag 클래스에 태그 풀에 존재하는 태그와 신규 태그를 분리할 수 있도록 했다. DB에서 태그 저장 및 조회를 하는 TagRepository는 다음과 같다. 일단 인터페이스만...
사용자가 입력한 태그를 파싱하고 태그 목록을 생성하는 TagProcessor를 다음과 같이 구현했다.
TagProcessor는 일반적으로 우리들이 사용하는 것처럼 TagService라는 명칭으로 봐도 무방하다. 위와 같이 구현하고 보니 Tag와 NewTag로 구현할 때보다 훨씬 단순화된 것은 사실이다. TagProcessor는 단지 Tag 관련된 작업만 하기 때문에 여기서 한단계 더 나아가 질문을 대표하는 Question 클래스와의 관계도 봐야할 듯 하다.
이 Question에 대한 DB와의 인터페이스를 담당하는 QuestionRepository
지금까지 구현한 모든 클래스를 엮어서 질문을 등록하고 수정하는 로직이 담겨 있는 QuestionService를 보면 다음과 같다.
위와 같이 구현했다. 지금까지의 구현을 봤을 때 어느 곳에서도 Tag 사용 수를 변경하는 부분이 없다. 최초 요구사항에서 다음 항목이 있다.
글을 등록할 때나 수정할 때 사용된 Tag에 대한 사용 횟수를 가감하는 로직은 어느 부분에 들어가는 것이 좋을까? TagProcessor에서 태그를 생성하는 시점이 좋을까? 이 시점에 하려면 수정의 경우 Question에 등록되어 있는 original tag를 전달해야 한다. 내가 처음에 만든 코드인데 썩 좋은 방법은 아니라고 생각한다.
그렇다면 현재와 같이 Question에 Tag Set을 전달하고 Question 내에서 이에 대한 로직을 처리하는 것이 맞을까?
지금까지 구현한 코드에 대해서도 추가적인 이슈가 있다면 제시해 주면 좋겠다. 이 글의 논쟁이 끝나면 현재 slipp.net에 구현되어 있는 코드 리팩토링 작업을 시작해 봐야겠다.
@자바지기 1. 테그 사용횟수 가감 : 이건 Tag클레스에서 하는건 어떨까?
추가로
그리고 위 코드와 같이 TagProcessor.java에서 신규 Tag를 조건분기 하는거 보단 TagRepository.findOrCreateTag(name)으로 하고 없을경우 생성해서 return해주는것도 괜찮을듯 한데? 동일하게 TagRepository.saveOrUpdate() 메소드도 하나 만들구....(너무 Hibernate냄새가 나나?) 아니면 TagFactory class를 하나 만들어서 그놈이 Tag클레스를 관리해도될듯은 하구...
내가 위와같이 하는게 좋을것 같다는 생각은 Tag는 그자체로 Domain의 성격을 가지고 있어서 NewTag가 따로 도메인이 필요하지 않을듯 해서다.(Tag로 모든게 해결 - 또한 이미 그렇게 바꾸기로 함) 그렇기 때문에 Tag를 사용하는 모든곳(Facroty나 Repository를 제외한)에서는 Tag가 최초 생성 Tag인지 기존Tag인지를 고려 하는 코드가 안들어 가는게 좋을것 같다.
@jhindhal.jhang 태그를 가감하는 부분은 다음과 같이 Tag에서 구현하고 있다.
여기서 이슈는 위의 tagged와 deTagged 메소드를 호출해야 하는데 어느 시점에 누가할 것이냐가 이슈이다. TagRepository.findOrCreateTag(name) 놈은 일단 밥 먹으면서 생각 좀 해보고..
@자바지기 TagRepository.save()(혹은 saveOrUpdate) 에서 하는게 좋지 않을까? 실제적으로 persistance로 저장되기전에 가감을 하는게 맞을듯 한데.
@jhindhal.jhang 밥 먹고 왔다. TagRepository는 단지 데이터베이스에 현재 상태의 데이터를 저장하고 조회하는 역할만 해야 되지 않을까? TagRepository에서 Tag의 상태까지 바꾸면 역할을 벗어나는 작업이지 않을까? TagRepository에 전달하기 전에 Tag의 상태는 이미 바뀌어 있는 것이 맞을 것이라 생각한다.
난 그 작업을 TagProcessor에서 하느냐, 아니면 Question 객체 내에서 하느냐가 좀 고민이 되네.
리포지토리가 저장하고 조회만 한다면 서비스에 비중이 커질 것 같네요. 심지어 DB의 특성이 서비스에 침투하게 될 수도 있기 때문에 리포지토리가 DB와 관련된 처리를 최대한 하는 것이 더 좋을 것 같아요.
그리고 한명의 사용자가 쓴다면 지금과 같은 구조라도 상관없겠지만 여러명이 사용한다고 가정한다면 태깅횟수나 신규태그여부에 대한 것도 DB중복체크 기능을 좀더 적극적으로 활용하는 것이 좋지 않을까요? 저 같으면 Tag객체의 count를 신뢰하기보다는 DB의 업데이트 결과를 더 신뢰하고 싶거든요.
한 가지 더 말씀드리면 parser 부분이 서비스에 있는 것도 좋은지는 모르겠네요. 서비스는 완전성 있는 객체들이 돌아다니는게 가장 깔끔한 것 같아요. 외부 프로토콜의 특성, 예를 들어 콤마로 구분될 수 있는 문자열,이 핵심비즈니스(서비스 같은 객체)에 영향을 미치는 것도 도메인을 빠르게 이해하는데 득이 되지 않는다고 생각하거든요. Controller는 프로토콜을 시스템의 인터페이스인 객체로 변환하는 역할도 담당해야 하지 않을까 생각해요. 예를 들면 아래처럼 ...
@benghun 신뢰성 측면에서는 저도 공감합니다. 저도 신뢰성 측면에서 태깅 횟수를 변경하는 부분을 db를 통해 변경할 수 있다고 생각합니다. 하지만 모든 경우가 그런 상황은 아니라 생각합니다.
ORM을 사용하지 않는다면 제공한 예제 소스와 같이 TagRepo에서 태그를 추가하고, 태그 수를 증가시켜도 무방하다고 생각합니다. 하지만 ORM을 사용하는 경우에는 Tag 객체의 상태만 변경하면 Transaction이 완료하는 시점에 변경된 상태 값을 데이터베이스에 자동 반영해 주기 때문에 구현할 소스 코드 양도 많지 않고, 테스트도 용이한 상태가 된다고 생각합니다. 이 경우는 ORM을 사용할 때와 사용하지 않을 때의 구현 방식이 약간은 달라질 수 있다는 생각이 드네요.
두 번째 경우는 String 문자를 Tag로 변환하는 부분을 QuestionService에서 담당하는 것이 아니라 Controller로 내리고 변환된 Tag Set을 QuestionService에 전달하는 인터페이스 측면에서 좋겠다는 의미로 받아들였습니다. 위 코드를 의견 주신 데로 변경한다면 다음과 같은 구조가 될 듯 합니다.
위 코드에서는 TagProcessor가 QuestionService와 의존 관계에 있었는데 지금은 Controller로 이동했습니다. 위와 같이 코드가 변경되면 QuestionService는 다음과 같이 변경되겠죠.
인자가 많은 부분은 Question 객체와 같은 형태로 묶어서 처리하도록 추후 변경한다고 보고요. 태그의 경우 String 문자를 직접 전달하는 것이 아니라 Tag Set으로 변환해서 위와 같이 전달하는 구조로 변경했습니다. 이 클래스에서는 TagProcessor와의 의존 관계는 없어질 겁니다.
저도 애플리케이션을 개발할 때 항상 이 부분이 고민입니다. QuestionService를 위와 같이 구현할 경우 create, update 메소드를 사용하는 클라이언트가 많아지면 이 클라이언트는 String 문자열을 Tag로 변환하는 작업을 항상 해야 되잖아요? 즉, 적절한 인자를 생성하기 위해 TagProcessor와 같은 API를 항상 인지하고 사용해야되는데 이 부분을 제가 처음에 구현했던 것처럼 QuestionService로 이동한다면 재사용성을 높일 수 있을 것으로 생각하는데요. 어떻게 생각하세요?
QuestionService는 핵심 Domain Layer를 구성한다고 보기 보다는 각 클래스간의 의존 관계를 연결해 주는 Thin Layer로 판단한다면 제가 최초 구현했던 다음 방식으로 진행하는 것도 좋지 않을까 생각합니다.
어차피 String 문자열을 Tag Set으로 변환하는 작업은 TagProcessor에 모두 위임하고 있기 때문에 그리 큰 이슈는 아니지 않을까 생각합니다. 그리고 문자열에 대한 검증은 TagProcessor가 담당하고 파싱 에러나 유효성에 문제가 있다면 Exception을 발생시킴으로써 인터페이스에 대한 안정성을 가져갈 수 있지 않을까요?
@자바지기 보여주신 코드는 충분히 좋은 코드라고 봅니다. 그렇기에 Service에서 파싱하는 로직이 있는 것도 나쁘다고 생각하진 않는데요. 말씀하신 것처럼 재사용성을 쉽게 높일 수도 있고ㅎ 다만 저는 잘못된 상태가 시스템의 깊은 곳까지 퍼지지 않았으면 하는 마음이 들더라구요. 아래 코드로 설명을 드리죠.
위 코드의 소소한 이점이라고 한다면 plainTags에 문제가 있다는 것을 시스템의 더 깊은 곳으로 보내지 않고 걸러낸다는 점이라고 생각해요. 서비스에 도착했을 때 최소한 객체 그 자체만으로는 올바른 값을 가지고 있다면 자잘한 사이드 이펙트를 방지하는데 도움이 될꺼라고 생각하거든요. 그리고 문제를 전파한 접점에서 예외를 발생시킴으로써 문제를 추적하는 측면에서 조금이나마 이점이 있지 않을까 하는거죠. 제가 바라는 점은요... 서비스 레이어에서는 모든 객체가 내부상태에 문제가 없고, 코드의 흐름을 보는 것이 마치 비즈니스를 읽는 것 처럼 느껴진다면 어떨까 하는 관점입니다. 서비스 레이어는 오직 비즈니스 논리의 개연성 문제만 고민하도록 만들고 싶다는거죠 ㅎㅎ
Question에 Tag가 추가되고 삭제될 때 Tag 사용 수를 가감하는 부분을 다음과 같이 구현해 봤다. ORM을 사용하지 않는 경우에는 일반적으로 Repository(또는 Dao)에 메소드를 호출해 변경하는 작업을 해야 되겠지만 ORM을 사용하는 경우 객체의 상태만 바꿔주면 Transaction이 종료되는 시점에 변경된 상태 값을 데이터베이스에 자동으로 반영해 준다. 따라서 다음과 같이 구현하는 것이 가능하다.
Tag 클래스 내부는 앞에서도 공개했지만 다시 한번 살펴보면 다음과 같다. 현재로서는 이 정도 수준으로 밖에 생각하지 못하겠다. Set를 직접 사용하는 것이 마음에 들지 않아 Tags라는 클래스를 만들어 사용할까도 고민해 봤지만 특별히 다른 역할을 할당할 부분이 보이지 않아 이 단계에서 멈췄다. 위 소스에서 taggedTags()와 deTaggedTags()를 Tags로 이동할까도 생각해 봤는데 오히려 역할이 맞지 않다는 생각을 했다. 그 보다는 Question 객체가 이에 대한 권한을 가지는 것이 맞지 않않을까 생각한다.
지금까지 단위 테스트 코드도 없고, 데이터베이스에 저장하는 로직도 없는 상태이다. 이 소스는 좀 더 다듬어서 slipp.net 소스에 반영할 수 있도록 해봐야겠다.
@자바지기 위 소스에서 deTaggedTags(Set tags), taggedTags(Set tags)에 있는 tags)로 새로운 테그를 넣어주는게 좋지 않을까?
Sets.difference(tags, originalTags);
이 부분은 한번 고민 해봐야 할듯 하다. 굳이 다른 테그들을 골라내는 로직이 필요 할까? 그냥 모두 deTaggedTags() 하고 taggedTags(Set이건 tag의 평균 갯수나 평균 변경 갯수 같은 데이터를 봐야겠지만. 대용량이 아니라면 굳이 다른 테크들만 찾아서 처리하는 것보다는 깔끔하게 다 지우고 다시 테깅 시키는게 좋을듯 한데..
@jhindhal.jhang 그 방법도 괜찮네. 어차피 메모리 상에서 숫자 값만 -1 됐다가 +1 되기 때문에 큰 성능 이슈는 없겠다. 데이터베이스까지 영향이 있으면 모르겠지만 ORM은 최종적으로 변경된 객체의 상태 값만 파악할 것이기 때문에 가능하리라 생각한다. 그럼 코드가 좀 더 단순해 지겠네. 어차피 하나의 질문에 입력할 수 있는 최대 태그를 5개로 제한하고 있기 때문에 데이터량도 많지 않아서 충분히 합리적인 방법이라 생각된다. 네 의견에 따라 리팩토링한 코드는 다음과 같다.
여기서 한 단계 더 리팩토링을 한다면 태그 처리하는 부분을 다음과 같이 리팩토링할 수 있겠다.
@자바지기 ORM을 안써봐서 그런지 신기하네요. 어떻게 싱크를 맞추지? ㅋㅋ
@benghun 조만간 slipp.net에 지금까지 논의한 내용들 반영하고 ORM 적용된 코드들 한번 공유할께요. 간단하게 설명하면 다음과 같이 합니다.
ORM은 데이터베이스에서 조회를 하면 조회한 객체를 Persistence Context라는 곳에서 관리합니다. 보통 Persistence Context(이하 PC)는 각 Session(보통은 Thread, Request 정도라 생각함 되요)마다 하나씩 생성되어 관리되는데요. 데이터베이스에서 한번 조회한 객체는 PC에 담겨 해당 Session내에서 다시 조회하면 데이터베이스 접근 없이 조회할 수 있습니다. 즉, 해당 요청 내에서는 Cache 역할을 하죠. 이건 그리 중요하지 않고요.
이와 같이 PC에 담겨 있는 인스턴스의 상태가 변경되면 ORM 프레임워크가 Transaction을 Commit하는 시점에 상태가 변경된 인스턴스를 찾아 데이터베이스에 자동으로 상태를 변경해 줍니다. 상태 변경이야 당연히 update 쿼리가 실행될 것이고요. 저도 이 부분은 깊이 있게 학습한 후에 공유해 보도록 할께요.
코드가 파편화되서...(댓글을 쭉 따라 읽었지만... 여러사람들의 코드가 뒤섞이다보니..ㅋ).. 전체 코드를 보면 좋겠지만... 처음에 생각했던 코드 모습이 그려진듯 합니다. (아래 코드 부분에서... 너무나 직관적이라 좋음..ㅎㅎ) private void newTags(Set tags) { deTaggedTags(originalTags); taggedTags(tags); this.originalTags = tags; newTags)로 하는 것이 덜 혼동되지 않을까요? 암튼 전체 코드가 궁금한데 볼 수 있는 곳이 있나요? 권한이 필요한가요? ㅋㅋ
} 그런데 메소드/파라미터명은 updateTags(Set
@ezblog slipp.net 소스는 https://github.com/javajigi/slipp에서 다운로드 받을 수 있다. 단, 지금까지 논의한 소스는 아직 적용되지 않았다. 현재 Tag와 NewTag로 분리되어 있어서 복잡도가 높다. 이 글에서 논의한 내용은 조만간 적용할 계획이다. 2월 중으로는 적용할 수 있지 않을까 기대한다. 그 때 받아보면 될 듯하다.
newTags 메소드는 최초 생성과 수정에서 모두 사용되고 있는데 변경한다면 addTags()가 더 적합하지 않을까 생각된다. 뭐 어떤 이름을 사용해도 딱 들어 맞지는 않는다. 어쩌면 메소드를 분리하는 것이 더 명확할지도... 지금은 코드량을 줄일려고 하나로 합친 느낌이 좀 있거든.
길어서 건성으로 좀 봤는데요...ㅋㅋ
눈에 띄는 것 하나가 Question클래스에서 originalTags 초기화하는 Set.newHashSet()이 왜 필요한지 잘 모르겠어요. 생성자에 파라미터가 전달되면 될거 같은데요.
public class Question { * private Set originalTags = Sets.newHashSet();*
@ologist 제가 생각해도 필요 없을 듯 하네. 그냥 습관적으로 구현해 놓은 듯하다. 참고해서 적용하도록 할께요.
지난 주 3박 4일 동안 부트캠프를 다녀왔는데 학생들이 활동하는 동안 할일이 없어서 여기서 논의한 소스 코드 반영하는 작업을 했습니다. 이 글과 같이 소스 코드를 간소화해서 논의할 때는 리팩토링이 직관적으로 보이고 쉽게 접근할 수 있었는데 전체 소스 코드에서 적용하려니 생각보다 잘 되지 않더군요. 리팩토링할 때 전체 소스 코드를 가지고 하는 것도 좋지만 이와 같이 간소화해서 논의한 후 방향을 정하고 리팩토링을 진행해도 좋겠다는 생각을 했습니다.
실 서비스에 적용한 소스 코드는 https://github.com/javajigi/slipp 에 있습니다. 혹시라도 관심 있으신 분들은 참고하시면 좋을 듯 합니다. 소스 보시고 이 글에서처럼 많은 의견 남겨 주시면 더 좋고요. 설치와 빌드 매뉴얼도 올려놨으니 빌드하시려는 분들은 문서 참고하심 됩니다. 혹시라도 잘 안되는 분 있으면 이야기해주세요.
@자바지기 이런 방식도 있어요. When I began working in this style, I had to give up the idea that I had the perfect vision of the system to which the system had to conform. Instead, I had to accept that I was only the vehicle for the system expressing its own desire for simplicity. -- http://c2.com/xp/OnceAndOnlyOnce.html
제가 영어를 못하지만 대충 발번역을 해보면 ... 내가 OAOO 방식을 따랐을 때, 내가 가진 시스템에 대한 완벽한 비전(시스템이 받아들여야만 하는)을 포기해야만 했다. 대신에 나는 단지 시스템을 위한 운송도구, 시스템 자신이 원하는 단순성을 표현하기 위한,임을 받아들여야 했다.
전체적인 방향성을 정하고 갈 수도 있겠지만 중복을 줄이겠다는 맹목적성을 가지고 가는 것도 공부에는 도움이 많이 되는거 같아요. ㅎㅎ
의견을 남기기 위해서는 SLiPP 계정이 필요합니다.
안심하세요! 회원가입/로그인 후에도 작성하시던 내용은 안전하게 보존됩니다.
SLiPP 계정으로 로그인하세요.
또는, SNS 계정으로 로그인하세요.