Java

제네릭

제네릭이란?

제네릭(Generic)은 클래스 내부에서 사용할 데이터 타입을 외부에서 지정하는 기법을 의미한다. 말이 어렵다. 아래 그림을 보자.

위의 그림은 아래의 코드를 간략화한 것이다.

package org.opentutorials.javatutorials.generic;

class Person<T>{
    public T info;
}

public class GenericDemo {

	public static void main(String[] args) {
		Person<String> p1 = new Person<String>();
		Person<StringBuilder> p2 = new Person<StringBuilder>();
	}

}

그림을 보자. p1.info와 p2.info의 데이터 타입은 결과적으로 아래와 같다.

  • p1.info : String
  • p2.info : StringBuilder

그것은 각각의 인스턴스를 생성할 때 사용한 <> 사이에 어떤 데이터 타입을 사용했느냐에 달려있다. 

클래스 선언부를 보자.

public T info;

클래스 Person의 필드 info의 데이터 타입은 T로 되어 있다. 그런데 T라는 데이터 타입은 존재하지 않는다. 이 값은 아래 코드의 T에서 정해진다.

class Person<T>{

위 코드의 T는 아래 코드의 <> 안에 지정된 데이터 타입에 의해서 결정된다. 

Person<String> p1 = new Person<String>();

위의 코드를 나눠보자. 아래 코드는 변수 p1의 데이터 타입을 정의하고 있다.

Person<String> p1

아래 코드는 인스턴스를 생성하고 있다. 

new Person<String>();

즉 클래스를 정의 할 때는 info의 데이터 타입을 확정하지 않고 인스턴스를 생성할 때 데이터 타입을 지정하는 기능이 제네릭이다. 

제네릭이 무엇인가를 알았으니까 이제 제네릭을 사용하는 이유를 알아보자. 

제네릭을 사용하는 이유

타입 안전성

아래 코드를 보자.

package org.opentutorials.javatutorials.generic;
class StudentInfo{
    public int grade;
	StudentInfo(int grade){ this.grade = grade; }
}
class StudentPerson{
	public StudentInfo info;
	StudentPerson(StudentInfo info){ this.info = info; }
}
class EmployeeInfo{
	public int rank;
	EmployeeInfo(int rank){	this.rank = rank; }
}
class EmployeePerson{
	public EmployeeInfo info;
	EmployeePerson(EmployeeInfo info){ this.info = info; }
}
public class GenericDemo {
	public static void main(String[] args) {
		StudentInfo si = new StudentInfo(2);
		StudentPerson sp = new StudentPerson(si);
		System.out.println(sp.info.grade); // 2
		EmployeeInfo ei = new EmployeeInfo(1);
		EmployeePerson ep = new EmployeePerson(ei);
		System.out.println(ep.info.rank); // 1
	}
}

그리고 아래 코드를 보자. 위의 코드는 StudentPerson과 EmployeePerson가 사실상 같은 구조를 가지고 있다. 중복이 발생하고 있는 것이다. 중복을 제거해보자.

package org.opentutorials.javatutorials.generic;
class StudentInfo{
    public int grade;
	StudentInfo(int grade){ this.grade = grade; }
}
class EmployeeInfo{
	public int rank;
	EmployeeInfo(int rank){	this.rank = rank; }
}
class Person{
	public Object info;
	Person(Object info){ this.info = info; }
}
public class GenericDemo {
	public static void main(String[] args) {
		Person p1 = new Person("부장");
		EmployeeInfo ei = (EmployeeInfo)p1.info;
		System.out.println(ei.rank);
	}
}

위의 코드는 성공적으로 컴파일된다. 하지만 실행을 하면 아래와 같은 오류가 발생한다.

Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to org.opentutorials.javatutorials.generic.EmployeeInfo
    at org.opentutorials.javatutorials.generic.GenericDemo.main(GenericDemo.java:17)

아래 코드를 보자.

Person p1 = new Person("부장");

클래스 Person의 생성자는 매개변수 info의 데이터 타입이 Object이다. 따라서 모든 객체가 될 수 있다. 그렇기 때문에 위와 EmployeeInfo의 객체가 아니라 String이 와도 컴파일 에러가 발생하지 않는다. 대신 런타임 에러가 발생한다. 컴파일 언어의 기본은 모든 에러는 컴파일이 발생할 수 있도록 유도해야 한다는 것이다. 런타임은 실제로 애플리케이션이 동작하고 있는 상황이기 때문에 런타임에 발생하는 에러는 항상 심각한 문제를 초래할 수 있기 때문이다. 

위와 같은 에러를 타입에 대해서 안전하지 않다고 한다. 즉 모든 타입이 올 수 있기 때문에 타입을 엄격하게 제한 할 수 없게 되는 것이다. 

제네릭화

이것을 제네릭으로 바꿔보자.

package org.opentutorials.javatutorials.generic;
class StudentInfo{
    public int grade;
	StudentInfo(int grade){ this.grade = grade; }
}
class EmployeeInfo{
	public int rank;
	EmployeeInfo(int rank){	this.rank = rank; }
}
class Person<T>{
	public T info;
	Person(T info){ this.info = info; }
}
public class GenericDemo {
	public static void main(String[] args) {
		Person<EmployeeInfo> p1 = new Person<EmployeeInfo>(new EmployeeInfo(1));
		EmployeeInfo ei1 = p1.info;
		System.out.println(ei1.rank); // 성공
		
		Person<String> p2 = new Person<String>("부장");
		String ei2 = p2.info;
		System.out.println(ei2.rank); // 컴파일 실패
	}
}

p1은 잘 동작할 것이다. 중요한 것은 p2다. p2는 컴파일 오류가 발생하는데 p2.info가 String이고 String은 rank 필드가 없는데 이것을 호출하고 있기 때문이다. 여기서 중요한 것은 아래와 같이 정리할 수 있다.

  • 컴파일 단계에서 오류가 검출된다.
  • 중복의 제거와 타입 안전성을 동시에 추구할 수 있게 되었다.

제네릭의 특성

복수의 제네릭

클래스 내에서 여러개의 제네릭을 필요로 하는 경우가 있을 것이다. 예제를 보자.

package org.opentutorials.javatutorials.generic;
class EmployeeInfo{
    public int rank;
	EmployeeInfo(int rank){	this.rank = rank; }
}
class Person<T, S>{
	public T info;
    public S id;
    Person(T info, S id){ 
        this.info = info; 
    	this.id = id;
    }
}
public class GenericDemo {
	public static void main(String[] args) {
		Person<EmployeeInfo, int> p1 = new Person<EmployeeInfo, int>(new EmployeeInfo(1), 1);
	}
}

위의 코드는 예외를 발생시키지만 문제는 다음 예제에서 처리하고 형식만 보자. 

즉, 복수의 제네릭을 사용할 때는 <T, S>와 같은 형식을 사용한다. 여기서 T와 S 대신 어떠한 문자를 사용해도 된다. 하지만 묵시적인 약속(convention)이 있기는 하다. 그럼 예제의 오류를 해결하자.

기본 데이터 타입과 제네릭

제네릭은 참조 데이터 타입에 대해서만 사용할 수 있다. 기본 데이터 타입에서는 사용할 수 없다. 따라서 아래와 같이 코드를 변경한다.

package org.opentutorials.javatutorials.generic;
class EmployeeInfo{
    public int rank;
	EmployeeInfo(int rank){	this.rank = rank; }
}
class Person<T, S>{
	public T info;
	public S id;
	Person(T info, S id){ 
		this.info = info;
		this.id = id;
	}
}
public class GenericDemo {
	public static void main(String[] args) {
		EmployeeInfo e = new EmployeeInfo(1);
		Integer i = new Integer(10);
		Person<EmployeeInfo, Integer> p1 = new Person<EmployeeInfo, Integer>(e, i);
		System.out.println(p1.id.intValue());
	}
}

new Integer는 기본 데이터 타입인 int를 참조 데이터 타입으로 변환해주는 역할을 한다. 이러한 클래스를 래퍼(wrapper) 클래스라고 한다. 덕분에 기본 데이터 타입을 사용할 수 없는 제네릭에서 int를 사용할 수 있다.

제네릭의 생략

제네릭은 생략 가능하다. 아래 두 개의 코드가 있다. 이 코드들은 정확히 동일하게 동작한다. e와 i의 데이터 타입을 알고 있기 때문이다.

EmployeeInfo e = new EmployeeInfo(1);
Integer i = new Integer(10);
Person<EmployeeInfo, Integer> p1 = new Person<EmployeeInfo, Integer>(e, i);
Person p2 = new Person(e, i);

메소드에 적용

제네릭은 메소드에 적용할 수도 있다. 

package org.opentutorials.javatutorials.generic;
class EmployeeInfo{
    public int rank;
    EmployeeInfo(int rank){	this.rank = rank; }
}
class Person<T, S>{
	public T info;
	public S id;
	Person(T info, S id){ 
		this.info = info;
		this.id = id;
	}
	public <U> void printInfo(U info){
		System.out.println(info);
	}
}
public class GenericDemo {
	public static void main(String[] args) {
		EmployeeInfo e = new EmployeeInfo(1);
		Integer i = new Integer(10);
		Person<EmployeeInfo, Integer> p1 = new Person<EmployeeInfo, Integer>(e, i);
		p1.<EmployeeInfo>printInfo(e);
		p1.printInfo(e);
	}
}

제네릭의 제한

extends

제네릭으로 올 수 있는 데이터 타입을 특정 클래스의 자식으로 제한할 수 있다.

package org.opentutorials.javatutorials.generic;
abstract class Info{
    public abstract int getLevel();
}
class EmployeeInfo extends Info{
	public int rank;
	EmployeeInfo(int rank){	this.rank = rank; }
	public int getLevel(){
		return this.rank;
	}
}
class Person<T extends Info>{
	public T info;
	Person(T info){ this.info = info; }
}
public class GenericDemo {
	public static void main(String[] args) {
		Person p1 = new Person(new EmployeeInfo(1));
		Person<String> p2 = new Person<String>("부장");
	}
}

위의 코드에서 중요한 부분은 다음과 같다.

class Person<T extends Info>{

즉 Person의 T는 Info 클래스나 그 자식 외에는 올 수 없다.

extends는 상속(extends)뿐 아니라 구현(implements)의 관계에서도 사용할 수 있다.

package org.opentutorials.javatutorials.generic;
interface Info{
    int getLevel();
}
class EmployeeInfo implements Info{
	public int rank;
	EmployeeInfo(int rank){	this.rank = rank; }
	public int getLevel(){
		return this.rank;
	}
}
class Person<T extends Info>{
	public T info;
	Person(T info){ this.info = info; }
}
public class GenericDemo {
	public static void main(String[] args) {
		Person p1 = new Person(new EmployeeInfo(1));
		Person<String> p2 = new Person<String>("부장");
	}
}

이상으로 제네릭의 기본적인 사용법을 알아봤다. 

댓글

댓글 본문
작성자
비밀번호
  1. 전민희
    제네릭 4,5 다시보기!
  2. 조서호
    제네릭은 다시봐야대
  3. 이태호
    7/17
  4. 뀨우
    멘붕했네요.. 나중에 필요하게 되면 다시 보러 올게요 ㅜㅜ
    좋은 강의 감사합니다.
  5. 김진홍
    감사합니다.
  6. ubiquitous4g
    Person p1 = new Person("부장");
    EmployeeInfo ei = (EmployeeInfo)p1.info;
    System.out.println(ei.rank);

    17 18번이 위 내용이 맞다는 가정하에, 일단 질문에, 해설부터 하자면,

    Person 클레스는 object 데이터 타입의 클레스입니다. (object 파트를 다시 보시면 됩니다.)
    Person p1 은 선언부로써 "인스턴스의 클래스 타입(Person) + 인스턴스명(p1)" 선언(준비)만 합니다.
    = new Person("부장"); 생성자로 ("부장")을 인수로 하는, Person의 인스턴스를 메모리에 로드(생성),
    Person p1 선언부의 p1 인스턴스명에 할당(연결)합니다.

    EmployeeInfo ei = (EmployeeInfo) p1.info;
    EmployeeInfo 클래스 타입의 ei 이라는 인스턴스를 선언하고,
    ei 라는 변수에 = (EmployeeInfo) p1.info; 할당(연결) 하게되면
    ei 는 앞서 만든 p1 인스턴스의 info 라는 변수를 참조하게 되고,
    p1.info 에는 값으로 "부장" 이 들어있는 부분과 ei 가 연결 됩니다.

    헌데 이때 (EmployeeInfo)라는 클래스 인스턴스 타입을 (EmployeeInfo) 사용해 제한(캐스팅) 합니다.
    문제는 p1 인스턴스의 info 는 object 형으로 아무것이나 받아들일 수 있고, 현제 String 인데,
    mployeeInfo 의 내부 맴버는 int 형이므로
    String"부장" 은 EmployeeInfo 의 int rank 와 데이터 타입이 호환이 안됩니다.
    그래서 오류가 납니다.

    일단 object 부분부터 다시 보셔야 할 것 같습니다.
    대화보기
    • 곧고수
      질문입니다..2번째 동영상에서
      17번과 그리고 특히 18번 어떻게 static도 아닌 인스턴스맴버를 클래스맴버처럼 ei.rank 로 바로 사용할수있는건가요??
      인스턴스화 시킨후에만 저렇게 사용가능한것이 아닌가요??ㅠㅠㅜ밑에 댓글중에 new person("부장")설명과는 상관없는 것 같은데요....
      아님 제가 무언가 착각하고있는거같은데 알려주세요~

      14. public class GenericDemo {
      15. public static void main(String[] args) {
      16. Person p1 = new Person("부장");
      17. EmployeeInfo ei = (EmployeeInfo)p1.info;
      18. System.out.println(ei.rank);
      19. }
      20. }


      아..! 그리고 17번 라인에서 object라는 부모클래스의 형이 employeeInfo라는 자식클래스의 형으로 변환할 수 있다는것도 이해가 안되네요....ㅠㅠ자식클래스만이 부모의 형태로 변환될수 있는 것이 아닌가요??..ㅠㅠㅠㅠㅠ
    • 하면된다하자
      제네릭 어렵네요.;;
    • 돌침대에서덤블링
      16 Person p1 = new Person("부장");

      여기서 Person 생성자 안에서 p1.info 가 인스턴스화 되잖아요.
      대화보기
      • 코딩천재
        훌륭한 강의 감사합니다
      • LazoYoung
        좋은 팁 감사합니다.
        대화보기
        • darkflame
          궁금한게 있습니다. 두 번째 동영상, 두 번째 코드 중,

          14 public class GenericDemo {
          15 public static void main(String[] args) {
          16 Person p1 = new Person("부장");
          17 EmployeeInfo ei = (EmployeeInfo)p1.info;
          18 System.out.println(ei.rank);

          17번 줄인, EmployeeInfo ei = (EmployeeInfo)p1.info; 에서 어떻게 인스턴스화 하지 않은 EmployeeInfo형 변수인 ei에 형변환된 값을 입력할수 있는가요?
          인스턴스변수는 인스턴스해야만 사용할 수 있는 것이 아닌가요?
        • popinbompin
          잘봤습니다
        • CalvinJeong
          아.. 마지막 Generic의 제한에서 의문점이 풀렸네요.
          Object 클래스를 부모로 사용했을때, class의 parameter로 개나소나 오만가지 다 올 수 있다는 문제점이 있다는 이유로 Generic을 사용하기 시작했는데,
          사실상 Generic의 선언 단계에서도 Generic 타입으로 오만가지가 다 올 수 있어서 .. 뭐가 의미가 있나 생각했는데
          Generic의 제한으로 이 문제를 해결할 수 있었던 거군요.

          egoing님의 강좌로 JAVA 정주행 완료해갑니다. 너무 감사해요
        • 효근
          나중에 다시 봐야겠습니다.
          자바는 협업이 아주 중요한 것 같네요
        • 민갱
          와.. 진짜 제네릭은 역대급이다.. 라고 느껴지는군요.. 이상하게 제네릭이 다른 파트보다 더 어렵게 느껴지는 1인입니다
        • 1234
          명강의 감사합니다
          강의 듣다가 궁금증이 든게
          Person클래스에서 제네릭을 사용안하고 생성자함수 인자타입을 인터페이스Info로주면 제네릭을 사용안하고도 똑같은 효과를 낼수있는건가 싶어서요 그렇다면 제네릭을 꼭 사용해야되나 하는 궁금증이드네요
        • GoldPenguin
          감사합니다.
        • 궁금이
          아래코드에 왜 에러가 발생하는지 잘 모르겠어요... 생략을 하면 왜 오류가 나는거죠?

          https://ideone.com/cpUGe7

          class EmployeeInfo4{
          public int rank;
          EmployeeInfo4(int rank){ this.rank = rank; }
          }
          class Person4<T, S>{ //generic으로 원시 기본 data type 을 사용할 수 없다. 사용하고 싶다면 wrapper class 을 사용해야한다. 원시 기본 data type을 객체화 시키는것
          public T info;
          public S id;
          Person4(T info, S id){
          this.info = info;
          this.id = id;
          }
          public <U> void printInfo (U info) {
          System.out.println(info);
          }
          }
          public class GenericDemo4 {
          public static void main(String[] args) { //Integer => int 에 대한 wrapping class

          System.out.println("");
          System.out.println("Multiple Generic Demo\n");

          EmployeeInfo4 e = new EmployeeInfo4(1);
          Integer i = new Integer(10);
          Person4<EmployeeInfo4, /*int*/Integer> p1 = new Person4<EmployeeInfo4, /*int*/Integer>(e, i);
          Person4 p2 = new Person4(e, i);
          //Person4 p1 = new Person4(new EmployeeInfo4(1), new Integer(10));

          //p1
          System.out.println(p1.info.rank);
          System.out.println(p1.id);
          System.out.println(p1.id.intValue());
          System.out.println("end\n");

          p1.<Integer>printInfo(e.rank);
          p1.printInfo(e.rank);
          p1.<Integer>printInfo(i);
          p1.printInfo(i);
          p1.<Integer>printInfo(i.intValue());
          p1.printInfo(i.intValue());
          System.out.println("end\n");

          //p2
          System.out.println(p2.info.rank); //=>오류 why?
          System.out.println(p2.id);
          System.out.println(p2.id.intValue()); //=>오류 why?
          System.out.println("end\n");

          p2.<Integer>printInfo(e.rank);
          p2.printInfo(e.rank);
          p2.<Integer>printInfo(i);
          p2.printInfo(i);
          p2.<Integer>printInfo(i.intValue());
          p2.printInfo(i.intValue());
          System.out.println("end\n");

          }
          }
        • 오징돌
          temp를 Integer로 수정하고 data의 타입을 오브젝트말고 T 로 바꿔보세요
          대화보기
          • 오징돌
            person<String> A = person<>은
            person클래스한테 스트링의객채만 전달할수있다고 제한하는 것입니다 그러니까 A는 person클래스의 객채인거죠
            대화보기
            • 감자돌이
              궁금한게 있습니다.

              Person A = new person();
              이렇게 인스턴스를 생성하면 A의 데이터타입은 기본적으로 Person이 되잖아요

              그런데 제네릭을 사용해서
              Person <string>A = new Person<string>();
              하게되면 A의 데이터타입을 string으로 정하는게 맞나요?

              그럼 Person은 더이상 A의 데이터타입 기능을 하지 못하는 건가요??

              컬랙션즈 프레임워크 강의를 듣다가, Collection<integer>A = new Hashset<Integer>(); 를 보며 hashset이 컬렉션 인터페이스를 구현하므로 좌항의 데이터타입을 컬렉션으로 해도 된다고 말씀하시는 부분에서 혼동이 와서 질문합니다.
            • 정동휘
              다들 '제네릭의 제한' 실습 되시나요??? 전 위의 코드로 잘 안되는 것 같습니다.
            • 파이팅
              한번들으니 멍하네요 . 지식이 쪼개지면서 받아들여짐 ㅜㅡㅜ
            • 엔터군
              질문이 있습니다.
              제네릭의 생략 동영상 강좌를 보면서 코드를 따라가는데요..

              Person<EmployeeInfo, Integer> p1 = new Person<EmployeeInfo, Integer>(e, i); 이코드를
              Person p1 = new Person(e,i); 로 바꿨는데 컴파일 과정에서..

              Note: GenericDemo.java uses unchecked or unsafe operations.
              Note: Recompile with -Xlint:unchecked for details.

              이러한 에러메세지가 떴는데요....혹시 다른분들은 안뜨시나요??

              -메세지 발생 원인-
              구글링 해보니 버전의 차이에서 발생되는 메세지라는 것을 찾았네요...
              클래스파일도 생성되고 실행도 되더군요...;;;;
              혹시나 다른이유가 더 있을지몰라서 질문글은 남겨둡니다
            • Jeon Dong Ho
              휴.. 막바지 들어서 어렵지만.. 좀더 살펴보면서 다시 내용을 정리하고 연습해 보겠습니다.
              좋은 강의 늘 감사합니다^^
            • 광대승천
              T가 가질수 있는 타입은 Info 혹은 Info의 자식 클래스가 됩니다.
              Person의 변수에 T타입의 info가 있죠
              Info 클래스는 getLevel() 메소드를 가지고 있기 때문에
              Info 타입 혹은 Info의 자식 타입의 Person 클래스의 변수 info 타입이 결정되면
              info.getLevel()을 사용할 수 있는 것으로 보이네요.
              혹시 틀린것 있으면 지적 부탁드립니다.
              대화보기
              • 광대승천
                맞아요 좀 이상하다고 생각했는데 이게 맞는것 같습니다.
                대화보기
                • 버미
                  그리고 아래 코드를 보자. 위의 코드는 StudentPerson과 EmployeeInfo가 사실상 같은 구조를 가지고 있다. 중복이 발생하고 있는 것이다. 중복을 제거해보자.

                  EmployeeInfo -> EmployeePerson
                • 라떼
                  어렵지만 더 공부를 해야겠네요 .. 감사합니다!
                • 헛둘헛둘
                  진짜 너무 좋네요 ㅠㅠ 동영상까지 올려주시고 감사합니다.
                • 자바바
                  갑자기 너무 어렵다;;
                • Weaver
                  강의 감사합니다~
                • SK Kim
                  Generic 계념은 이해가 되는데, 설명 예제들의 구조가 오히려 햇갈리네요.
                  클래스 내부에 클래스가 있고 하니..
                  초보가 사용하기에는 고급 내용인듯하니 다음 강의로 넘어갑니다.
                • JustStudy
                  고맙습니다
                • 감사합니당ㅇ
                • 김트라슈
                  감사합니다~
                • 보람차게
                  IDE 인텔리제이 쓰고 있는데 제네릭 생략하면 Person(T,S)의 요청이 확인되지 않는다는 경고문구가 뜨네요. 그리고 p2.info.rank가 해결되지 않는다고 에러가 발생하는데 왜 이런걸까요?
                  p2.info
                  p2.id
                  는 잘 되는데 말이죠...
                • 보람차게
                  Java5 이상부턴 new Integer(1)로 박싱 안 하고 그냥 1로 보내도 된다는군요
                • 오빠는다르다
                  감사합니다!!!!!!
                • ㅇㅇ
                  작년에 처음들었을때는 도저히 이해가 안갔었는데
                  이번에 두번째로 들으니깐 보이긴 보이네요
                  막상 나중에 쓸려고 하면 또 어려울듯 ㅋㅋ
                • 박첩구드
                  제네릭은 정말 유용할 것 같아요~!!
                • flys0m@naver.com
                  자바 넘나 어려운것
                • eagles70
                  좀 어려워 지네요^^;;
                • scs87
                  안녕하세요 이고잉 강사님 !

                  두 번째 강의에서 StudentPerson 과 EmployeePerson 로직의 형태가 중복되고 있는데요~.

                  EmployeePerson 대신 EmployeeInfo로 잘못 메모 되어 있는 것 같아서요

                  수정해주시면 더 좋은 강의가 될 것 같아서요 !

                  무튼 감사합니다. 아 그리고 지금까지 강의 전부 만족하는데요 ^^

                  enum강의만 꼭 좀 보완 해주시면 감사해요

                  노트에는 나와있는데 설명을 안해주시는 부분도 있더라구요

                  그럼 수고하세요 파이팅 ^^
                • 방문자
                  String Integer 둘이 다르기 때문이 아닐까여
                  대화보기
                  • dkiekkf@gmail.com
                    클래스에서는 T S 를 사용하셨고 메서드에서는 U를 사용하셨는데 각각의 T S U가 용도가 틀린가요 아니면 전부다 generic을 의미하는 대문자 같은건가요?
                  • cocohodu
                    좋은강의 감사합니다
                  • 나무늘보
                    유튜브 오른쪽 하단에
                    HD라고 뜬 톱니바퀴모양
                    속도 <<<<<<<<<<
                    대화보기
                    • 배속
                      배속으로 보는법좀요
                      대화보기
                      버전 관리
                      egoing
                      현재 버전
                      선택 버전
                      graphittie 자세히 보기