제 홈페이지의 모든 글은 anti-nhn license에 따릅니다.



대용량 서비스 - 1. 적당한 데이터 클래스

프로그램에서는 데이터를 다룰 일 들이 참 많습니다. 필요에 따라 적절한 데이터 타입을 선언하는 게 매우 중요할 수 있습니다.

1. 모여야만 의미가 있는 것은 클래스로 만들자.

위도와 경도 데이터가 있다고 칩시다. 이는 어떤 위치을 표현하기 위한 것인데, 위도만으로도 경도만으로도 의미가 없습니다. 게다가 위도와 경도는 생김새도 매우 비슷합니다.(float이나 BigDecimal 등으로 표현 가능한 값들입니다.) 즉, 바꿔 쓰는 실수를 하기가 쉬운 경우입니다.
따로 따로 의미를 가질 수 없는 경우는 이런 데이터를 담을 수 있는 클래스를 별도로 만드는 게 좋습니다.

대략 다음과 같은 클래스가 정의가 될 겁니다.

import java.math.BigDecimal;

public final class Location {
 private final BigDecimal lat; //위도
 private final BigDecimal lng;  //경도

 public Location (BigDecimal lat, BigDecimal lng) {
  this.lat = lat;
  this.lng = lng;
 }

 public BigDecimal getLat() {
  return lat;
 }

 public BigDecimal getLng() {
  return lng;
 }
}

이렇게 만들면 따로 따로 쓰는 것보다 훨씬 안전합니다.

아래와 같은 함수 호출 스택을 타고 처리하는 과정이 있다고 가정합시다. 아래에서 Request 클래스는 lat과 lng의 최초값을 가지는 클래스라고 칩니다. HttpServletRequest 같은 걸 생각하시면 됩니다.
foo1 (Request req) -> foo2 (BigDecimal lat, BigDecimal lng) ->  ... -> fooN (BigDecimal lat, BigDecimal lng)

fooN 에서 lat과 lng이 바뀐 것을 발견했습니다. 과연 어디서 잘못된 것일까요? foo1 부터 fooN까지 다 뒤져봐야 합니다. 그러나, 함수가 다음과 같이 되어있다고 가정하면 얘기가 다릅니다.

foo1 (Request req) -> foo2 (Location point)  -> ...-> fooN (Location point)

fooN에서 잘못되었다면,foo1에서 Location 클래스를 만들 때 잘못 만들었거나 fooN에서 거꾸로 받아썼거나 둘 중 하나 입니다. (물론 중간에 Location 객체를 이상하게 만들어 내서 인자를 바꿔서 전달하는 것도 가능은 하지만 별로 현실적인 얘기는 아닙니다.)

즉, 프로그램이 잘못되었을 때 점검해야 할 포인트가 확 줄어듭니다.

이런 데이터 클래스를 만들 때는 다음 사항에 주의해야 합니다.

1. hashCode와 equals 구현이 필요한 지 고려해야 합니다.
HashMap이나 캐쉬 등에서 키로 사용될 여지가 있을 경우 구현해야 합니다. 이클립스의 기능을 이용하면 쉽습니다. 소스에서 오른쪽 클릭 -> Source -> Generate hashCode and equals  를 누르면 손 코딩 안해도 됩니다.

2. Serializable을 고려해야 합니다.
일반적인 데이터 객체는 하드디스크나 디비에 저장되거나 외부와 통신에 사용될 여지가 있습니다. 이럴 경우 Serializable을 구현해주어야 합니다. 아...Serializable은 한번 꼭 정리해야 할 것 같은데... 나중에 정리되면 링크 달겠습니다.ㅜㅜ

3. Cloneable을 고려해야 합니다.
어떤 데이터 객체는 복사되어 값이 살짝 수정되어 쓰인다거나 할 일이 있습니다. 또는 Prototype 패턴으로 객체가 생성되는 케이스도 있을 수 있습니다. 자세한 것은 요기를 참고해주세요.^^

4. 가능하면 Immutable로 만드는 게 좋습니다.
이건 요기를 참고해주세요.  

5. toString 을 반드시 구현하세요.
예제는 코드가 길어져서 생략했지만, 로그 등을 찍을 때 toString 을 구현하는 게 매우 좋습니다. 

2. Map 이나 List 등 Container 사용을 자제하자.

위와 같은 Location을 맵으로 만들면 다음과 같이 될 겁니다. 아래에서 request는 대충 뭔가 요청을 담고 있는 클래스의 인스턴스라고 칩니다.

Map<String, BigDecimal> location = new HashMap<String, BigDecimal>();
location.put("lat" , request.getParameter("lat"));
location.put("lng" , request.getParameter("lng"));

조금 전에 언급한 사항 들이 대부분 해결되었습니다. hashCode나 equals는 HashMap이 알아서 해줄 것이고, Serializable도 해결되었고, Cloneable도 해결되었습니다.(Cloneable 은 변수 타입을 Map 이 아니라 HashMap으로 하면 됩니다.)
Immutable 문제만 빼고는 깔끔하게 해결된 듯하지만 위와 같은 코드는 바람직하지 않습니다.

1. 데이터 클래스는 변경이 용이합니다.
기존의 Location 클래스에 주소 정보까지 넣는 걸로 바꾸려고 합니다. 시도/구군/동면 등의 정보를 넣으려고 하면 Location 객체를 쓸 경우 Location 객체 안에서만 바꿔주면 됩니다.(위와 같은 경우 Location 클래스의 Constructor에서 주소정보를 가져오는 부분을 추가하고 각각의 단위 주소에 대해서 getter 들을 만들어 주면 됩니다.)  하지만 Map을 쓸 경우 저렇게 만들어지는 클라이언트 코드를 전부 찾아서 바꿔야 합니다.
이것보다 더 큰 문제는 특정 필드를 제거할 때 생깁니다. 필드를 제거하게 되면 클래스에서는 멤버 변수 1개와 getter 1개 정도 제거하면 될 것이고, 제거된 함수를 쓰는 부분이 어딘지는 금방 알아낼 수 있습니다.(이클립스가 다 결켜줍니다.^^ compile 에러니까요.)
하지만 Map을 쓰면 ............................ 그냥 제거하지 마십시오..........

2. 데이터 클래스는 에러를 더 빨리 잡아낼 수 있습니다.
각각의 필드에 대해서 명확한 getter 메쏘드가 존재합니다. 하지만 Map을 쓰게 되면, get("lat") 과 같이 인자로 던져줘야 하는데 인자가 String이므로 오타가 나더라도 프로그램 실행을 해봐야 알 수 있습니다. Compile 타임에 에러를 잡아낼 수 있는 것과 Runtime에 에러를 잡아낼 수 있는 것의 차이가 생깁니다.

3. Map은 데이터의 일관성을 보장해 주지 않습니다.
Map은 <키, 값> 구조이므로 상황에 전혀 적절치 않는 데이터가 들어가는 것도 허용합니다. 위와 같은 코드에 location.put("owner", new BigDecial("8001011234567)); 과 같이 그 위치의 건물에 대한 주인의 주민번호를 BigDecimal로 표현한 이상한 값도 넣을 수 있습니다. 그나마 generic이 있기 때문에 이렇지 generic마저 없었으면... 매우 끔찍한 사태가 발생할 수도 있습니다.
또, 상황에 따라 get("뭐시기")를 했을 때 있을 수도 없을 수도 있습니다.반드시 뭔가 들어가 있다는 것을 보장해주지 않습니다. 이에 반해 데이터 클래스를 사용하면 Constructor에서 유효성 검사를 할 수 있습니다.

4. 속도도 느려지고 메모리 낭비가 생깁니다.
Map에서 데이터를 뽑는 게 결코 데이터 클래스의 getter보다 빠를 수 없고, Map은 어떤 식으로든 성능을 위해서 일단 데이터 클래스보다 더 많은 메모리를 확보합니다.


이보다 더 황당한 다음과 같은 코드도 있습니다.

List<BigDecimal> location = new ArrayList<BigDecimal>();
location.add(request.getParameter("lat"));
location.add(request.getParameter("lng"));

0번째 값은 위도 1번째 값은 경도로 쓰는 겁니다. 절대...절대.. 이러지 마십시오..

Container는 일반적으로 generic을 사용해 표현이 가능한데 <String> 과 같이 변수의 타입만 설정합니다. String은 사람의 이름을 표시할 수도 있고, 나라 이름을 표시할 수도 있습니다. Container를 사용할 때는 generic에 들어갈 것이 변수 타입이 아니라 표현할 데이터가 무엇인지를 정확하게 정의할 수 있을 때만 써야합니다.
즉, Map<String으로 표현된 사람이름, String으로 표현된 나라이름> 과 같이 generic 안의 타입뿐 아니라 타입이 의미하는 바가 정확히 무엇이다라고 정의할 수 있는 경우에만 Container를 사용하셔야 합니다.

3. 일반적으로 init함수는 틀렸다.

위와 같은 Location 클래스에서 주소 정보를 가져오는 로직이 들어갔다고 칩시다.(매우 무겁게도 구글 같은데에 통신을 해서 정보를 조회해야 할 겁니다.)
이런 경우 init 함수를 만들고 그 함수를 호출하면 구글에 통신해서 주소 정보를 세팅하도로 프로그램을 하는 경우가 많습니다. 하지만 주소 값이 반드시 필요하다면, Constructor에서 하면 됩니다.

주소값이 경우에 따라 필요하다면 다음 두 가지 해결 방법이 있습니다.

1. 주소에 관련된 getter에서 위치값이 설정되었는 지 확인합니다. 다음과 같은 코드가 가능합니다.

 private String address;
 public String getAddress() {
  if (address == null) {
   setAddressUsingHttp();
  }
  return address;
 }

 private void setAddressUsingHttp() {
     // 뭔가 통신해서 address라는 값을 세팅해주는 로직
 }

2. 또 다른 데이터 클래스를 만듭니다. 이 경우 기존의 Location 클래스를 Builder 처럼 이용하면 됩니다.

public final class Location{
 private final BigDecimal lat;
 private final BigDecimal lng;
 public Location(BigDecimal lat, BigDecimal lng) {
  this.lat = lat;
  this.lng = lng;
 }

 ... getter 들
 
 public LocationWithAddress getLocationWithAddress(){
  return new LocationWithAddress();
 }
 
 public class LocationWithAddress{
  private final String address;
  
  private LocationWithAddress(){
   // Constructor 안에서 http를 이용해서 lat, lng 으로 address를 읽어와서 세팅..
  }

  //Location 클래스에도 있는 method들..
  public BigDecimal getLat() {
   return lat;
  }
  public BigDecimal getLng() {
   return lng;
  }
 }
}


위와 같이 하면 LocationWithAddress라는 클래스의 생성자가 private 이므로 외부에서 생성이 불가능합니다. 오로지 Location을 통해서만 생성 가능하기 때문에 뭔가 잘못되었을 때 찾아내기가 쉽습니다.

보통의 init 함수는 대부분 Constructor에서 해결 가능한 것입니다. 게다가 일반적으로는 init을 호출하지 않으면 뭔가 문제가 생기도록 init 함수를 만드는데 이는 프로그램을 복잡하게 만들며,에러 가능성도 증대시킵니다.

그러면 servlet 이나 servlet filter 등에서도 init을 쓰는데 이것은 바른 사용일까요?
이 경우는 괜찮습니다. servlet이나 filter는 데이터형 클래스가 아닙니다. 즉, 사용자가 직접 Constructor를 호출한다거나 하는 식으로 instanciate 하지 않습니다. 게다가 모든 lifecycle이 톰캣 등에서 관리가 되기 때문에 이런 경우는 init이 호출되었는지 안 되었는지 등에 대해서 사용자가 신경쓸 게 없습니다.
Constructor는 직접 호출하지 않는 이상 상속이 안 됩니다. 따라서 상속 가능한 init 이라는 함수를 만들어서 쓰는 게 이런 경우는 훨씬 깔끔합니다.



정리하며..

사실 이번 글에서 하고 싶었던 얘기는 General한 프로그램과 Specific 한 프로그램에 대한 얘기였습니다. General 하다는 것은 위에서 얘기한 Map 같은 컨테이너를 쓰는 것과 같은 겁니다. 어징간하면 다 받아주는 거죠. Specific하다는 것은 위에서 Location 클래스와 같은 것을 만들어 쓴다는 것입니다. 딱 지정된 것만 받아들이는 겁니다.
일반적으로 General 함을 더 바람직한 것으로 생각하는 경향이 있는데, 결코 그렇지 않습니다. 기본적으로는 Specific한 케이스를 고려하고 거기서 더 general하게 풀어줄 수 있는 영역을 찾아내야 합니다.

by 삼실청년 | 2011/10/02 18:30 | 컴터질~ | 트랙백 | 핑백(1) | 덧글(6)

트랙백 주소 : http://iilii.egloos.com/tb/5553669
☞ 내 이글루에 이 글과 관련된 글 쓰기 (트랙백 보내기) [도움말]
Linked at happy2v : [Link].. at 2012/02/17 21:21

... 삼실 청년 블로그대용량 서비스 만들기 - 1. 적당한 데이터 클래스 ... more

Commented by 용식 at 2011/10/27 11:16
좋은 글 잘 읽었습니다~
언제 읽어도 설명을 참 잘 하시는 것 같아요.
쉽고 간단하게요.. ㅎㅎ

List를 사용한 데이터 클래스를 후배가 사용하려고 했었던 기억이 나네요..

전 그걸 "우연에 의존한 프로그래밍"이라고 했었어요.. ㅋㅋ
3번째 index에 뭐가 들어있는 줄 아냐고 물어보면서 말이죠..ㅎㅎㅎ
Commented by 삼실청년 at 2011/10/27 21:34
바람직한 선배시군요^^

누가 갈켜주지 않으면 10년이 지나도 저런 게 좋다고 생각하고 있었을 지도 모를 일이죠. 따꼼하게 한 말씀 날려줘야죠ㅋ
Commented by 미스터리 at 2012/01/06 11:30
많은 걸 배우고 갑니다.^^
Commented by 삼실청년 at 2012/02/21 00:26
이글이 정말 별거 아닌 거 같은데 굉장히 중요한 내용이에요.

"돌아가는 프로그램"과 "잘 만든 프로그램"를 나누는 기준 중 하나죠.
Commented by happy2v at 2012/02/17 21:24
블로그 정말 정리 잘 되어 있네요 ^^ 저도 오늘 이글루스 블로그 만들고 글 몇개 퍼갑니다. 좋은 자료 감사합니다.
Commented by 삼실청년 at 2012/02/21 00:28
수년에 걸쳐.. 한두달에 하나씩 쓴 글인데다가.. 가끔 누가 지적해주면 과감히 고치고, 혼자서 오타 수정도 하고, 잘못 쓴 거 알고 나중에 고쳐쓰고.. 그러다가 보니 조금씩 정리가 되어가네요. (제가 잘못쓴 글이 인터넷 어딘가에서 발견될 때, 가슴이 아픕니다...만..... 모른 척하고 지나갑니다.ㅋㅋ)

:         :

:

비공개 덧글

◀ 이전 페이지          다음 페이지 ▶