ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Java-27] 자바 생성자에 대한 고찰
    Java 2020. 12. 18. 19:00

    Java New Keyword

    🐵 자바 생성자에대한 고찰 (this, Builder, Overload)


    생성자는 이전에 설명한 new 연산자와 가이 사용하고, 객체를 생성하는 역할과 객체 초기화 역할을 한다.
    생성자가 제대로 실행도지 않는다면, 객체의 주소값이 리턴도지 않을 뿐더라, 객체가 heap에 올라가지도 않을 것이다.


    기본적인 생성자


    기본생성자 Class()

    기본적으로 생성자를 정의해주지 않는다 하더라도, Object 를 상속받은 모든 클래스들은 기본 생성자 라는걸 가지고 있다. 예를 한번 들어보자

    public class App // 생성자 in Object class
    {
         A a = new A();
        B b = new B();
        C c = new C();
        D d = new D();
        E e = new E();
        F f = new F();
        G g = new G();
    
        class A{ } //생성자 in App class
        class B{ } //생성자 in App class
        class C{ } //생성자 in App class
        class D{ } //생성자 in App class
        class E{ } //생성자 in App class
        class F{ } //생성자 in App class
        class G{ } //생성자 in App class
        public static void main( String[] args )
        {
            App app = new App();
        }
    }

    App 이라는 클래스 안에서 A~G 까지의 내부 클래스들을 정의하을 때, 여기있는 이너클래스들의 생성자는 App.class 에 정의 되어있고, 반대로 App 클래스는 그의 상속받은 클래스인 Object 클래스에 정의되어 있다.

    생성자의 오버로딩

    생성자는 기본적으로 제공하는 즉, 기본생성자 (Defaulf Constructor)를 제외하고도 개발자게 임의로 생성자를 정의할 수 있다. 한가지 예시코드를 보도록 해보자

    public class Point {
        int x;
        int y;
        int z;
        // default Constructor
        public Point(){}
        // overload Constructor 1
        public Point(int x, int y, int z){
            this.x = x;
            this.y = y;
            this.z = z;
        }
        // overload Constructor 2
        public Point(int x,int y){
            this.x = x;
            this.y = y;
        }
        // overload Constructor 3
        public Point(int x){
            this.x =x;
        }
    }

    이런 코드 가 작성되어 있다고 생각해보자, 기본생성자 말고도 다른 생성자들이 많이 정의되어있는데, 생성자에 들어가있는 매게변수에 따라서, 우리는 이 4가지의 생성자를 마음대로 사용할 수 있다. 이게 어떻게 가능한 걸까? java에서는 다형성 을 지원하기 위해서 overload(오버로딩) 이란 기능을 지원한다.
    오버 로딩이란, 매게변수가 다른 여러개의 메소드를 작성할 수 있고, 이를 마음대로 사용할 수 있다는 것이다. main에서 이 모든 생성자를 사용해보도록 하자

        public static void main(String[] args) {
            Point 기본생성자 = new Point();
            Point 매게변수생성자xyz = new Point(1,2,3);
            Point 메게변수생성자xy = new Point(1, 2);
            Point 메게변수생성자x = new Point(1);
        }

    이렇게 작성한 생성자 는 X, Y, Z 를 모두 받는 생성자와 , X,Y, 그리고, X만 받는 생성자를 모두 사용할 수 있다. 근데 뭔가 불편하지 않는가? 생성자를 통해서 class 안에 있는 필드를 초기화 시켜줄 수 있는건 좋은데, 만약 내가 여기서 초기화 시킬 고 싶은 필드가 달라진다면 그 때마다 생성자를 오버로딩 해야하는 단점 이 있다.

    단점

    public class Point {
        int x;
        int y;
        int z;
    
        public Point(int x, int y, int z){
            this.x = x;
            this.y = y;
            this.z = z;
        }
    
        public static void main(String[] args) {
            int y = 30;
            Point point = new Point(30); // Error -> 매게변수 하나를 받는 생성자가 없음 
            // y를 초기화 시키는 객체 생성 
        }
    }

    그럼 만약 int y를 매게변수로 받는 생성자를 만든다고 가정해보자

    public class Point {
        int x;
        int y;
        int z;
    
        public Point(int y){
            this.x = y;         // 실수로 x를 필드 값을 받는 생성자를 생성
        }
    
        public static void main(String[] args) {
            int y = 30;
            Point point = new Point(30); // 생성자가 y를 초기화하지 않고 x를 초기화함 !!
        }
    }

    우리가 y 맴버변수를 받는 파라미터 생성자를 만들어야 하는데 실수로 맴버변수 x를 받는 생성자를 만들었다고 했을 때 개발자가 직접 생성자를 까보지 않는 이상 버그발생 시 뭐가 문제인지 알 수가 없다.

    빌더패턴?

    이런 생성자의 실수를 줄이고, 여러개의 생성자를 생성하지 않도록 많들 수 있는 생성자 정의 방법이 있다. 빌더 패턴을 적용하여, 자바의 다형성과, 개발자의 편의성을 보장할 수 있다. 일단 빌더 패턴을 적용한 코드를 확인해 보자

    public class Point {
        int x;
        int y;
        int z;
    
        public Point(Builder builder) {
            x = builder.x;
            y = builder.y;
            z = builder.z;
        }
    
        public static class Builder{
           private int x =0;
           private int y =0;
           private int z =0;
    
           public Builder x(int x){
               this.x = x;
               return this; // . 으로 메소드 체이닝
           }
           public Builder y(int y){
               this.y =y;
               return this;
           }
           public Builder z(int z){
               this.z = z;
               return this;
           }
           public Point build(){
               return new Point(this);
           }
       }
    }

    Builder 를 static inner클래스로 지정하고 그 안에서 return this; 를 통해서 메소드 체이닝을 걸어서 매게변수를 받을 수 있도록 작성할 수 있다. 그리고 마지막으로 .build() 를 통해서 객체를 반환하는 방법이 있다. 한번 적용된 코드를 보도록 해보자

    빌더 테스트

        @Test
        void testBuilder(){
            Point point1 = new Point.Builder().x(1).y(2).z(3).build();
            // X, Y, Z, -> 1, 2, 3 으로
            assertEquals(point1.getX(),1);
            assertEquals(point1.getY(),2);
            assertEquals(point1.getZ(),3);
            Point point2 = new Point.Builder().y(2).build();
            // Y -> 2 로
            assertEquals(point2.getY(),2);
            Point point3 = new Point.Builder().z(34).build();
            // Z -> 34 로
            assertEquals(point3.getZ(),34);
        }

    대단하지 않나? 자신이 원하는 매게변수만 받는 생성자를 정의할 필요없이 빌더 패턴을 이용해서 해결할 수 있지 않은가?

    Q : 아니? 안편해, 빌더 패턴 저거 언제다 정의하냐?

    A : 롬복 ㄱㄱ

    롬복을 사용하는게 더 좋다고 할 수는 없다. 일각에서는 롬복을 사용하는걸 꺼리는 개발자가 있다고 하니 말이다, 하지만 효율성을 생각하면 난 롬복을 사용하는걸 좋아한다. 적어도 getter, setter, builder 에서는 말이다. 자 아까 정의한 길고 긴 빌더패턴을 어노테이션 한방으로 해결해줄 수 있다.

    • 롬복은 개발자가 작생해야하는 게터, 세터, toString, EqualsAndHashcode 를 어노테이션 하나로, class 파일을 조작(정상적인 방법은 아니긴 하다...)하여 개발자의 편으를 도와준다. 한번 사용법을 알아보도록 하자. ps 디펜던시를 추가하는 방법은 따로 작성하진 않겠습니다.!
    @Getter @Builder // <- 빌더 어노테이션 설정
    public class Point {
        int x;
        int y;
        int z;
    
        public static void main(String[] args) {
            Point point = new PointBuilder().x(1).build(); // <- @Builder 가 생성해준 빌더패턴
            //  Point point3 = new Point.Builder().z(34).build(); <- 직접 작성한 빌더패턴
        }
    }

    이렇게 간단하게 빌더패턴을 @Builder 를 통해서 정의할 수 있다.

    • 사실 여기서 설명하는 곳보다 빌더패턴에 대해서 정말 자세히 설명한 유튜브 영상이 있으니 참고하면 너무 좋다.

    • 여기까지 우리가 생성자를 어떻게 만들 수 있을까에 대해 생각을 할 수 있는 시간이 되었다. 근데 좀 더 자세히 생성자에 대해서 알아보도록 해보자

    Q : Overloading 한다했는데, 그럼 생성자는 메소드인건가?

    A : 그러기도 하고, 아니기도 하다.

    • 상속을 통한 오버라이딩이 불가능하다.
    • 객체를 heap 메모리에 띄우고, 객체의 주소값을 반환하기 때문에 따로 return을 줄 수 없다.
    • public, protected, private는 사용할 수 있지만 void, native, static 같은 수식어, return타입은 사용할 수 없다.
    • 여러 자바가 메소드인가 아닌가에 대한 얘기가 참 많다. 일단 나는 전적으로 메소드라고 생각을 한다. method라고 설명하는 글들이 많기도 하다.
      image
    • 무엇보다 바이트코드에서는 생성자를 메소드로 정의하고 있기 때문에, 생성자를 메소드라고? 생각하는 쪽에 손을 들어주고 싶다.
    public class Point {
        int x = 0;
        int y = 2;
        public Point(int x){
            this.x = x;
        }
        public Point(){
        }
        public Point(int x, int y){
            this.y = y;
            this.x = x;
        }
    }
    //Byte Code 
    Constant pool: // jvm 상수 풀
       #1 = Methodref          #8.#30         // java/lang/Object."<init>":()V
       #2 = Fieldref           #4.#31         // org/example/Point.x:I
       #3 = Fieldref           #4.#32         // org/example/Point.y:I
       #4 = Class              #33            // org/example/Point
       #5 = Methodref          #4.#30         // org/example/Point."<init>":()V
       #6 = Methodref          #4.#34         // org/example/Point."<init>":(I)V
       #7 = Methodref          #4.#35         // org/example/Point."<init>":(II)V
       #8 = Class              #36            // java/lang/Object
    

    매개변수하고 맴버변수가 같는데 대입이 어떻게 가능한가?

    public class Point{
        int x;
    
        public Point(int x){
            x = x; 
        }
    }

    만약 이런 코드가 있다고 가정해보자 생성자에서 받은 파라미터가 x인데, x = x 로 x 변수에 대입을 했을 때, 이 x 는 도대체 뭘 가르키는 걸까?

    • Point 클래스 안에있는 맴버변수 x 인가?

    • Point 생성자의 파라미터 x 일까?

      현재 저 코드에서의 x는 class point 의 x를 가르치지 않고 생성자 Point의 파라미터 x를 가르치게 된다.

      그렇다고 public Point(int var) 와 같이 변수를 지정해주는 것도 변수 네이밍을 지정해주는 거에서도 꽤 귀찮을 일이다.

    this 로 해결

    public class Point{
        int x;
    
        public Point(int x){
            this.x = x; 
        }
    }

    이렇게 x 앞에 this. 으로 체이닝을 걸어 주었다. 그럼 이 x는 어떤 변수를 의미하는 걸까? 그건 바로 class Point 의 x를 가르킨다. this를 앞에 걸어줌으로 써

    • 이 변수는 의 객체의 맴버변수를 의미합니다. 한번 재미삼아 코드를 보자
    @Getter
    public class Point {
        int x = 0;
    
        public Point(int x){
            x = x;
        }
    }
    
    class PointTest {
        @Test
        void testBuilder(){
            Point point = new Point(1234);
            assertEquals(point.getX(),1234);
        }
    }
    // testing 결과 실패
    org.opentest4j.AssertionFailedError: 
    Expected :0
    Actual   :1234
    
    @Getter
    public class Point {
        int x = 0;
    
        public Point(int x){
            this.x = x;
        }
    }
    class PointTest {
        @Test
        void testBuilder(){
            Point point = new Point(1234);
            assertEquals(point.getX(),1234);
        }
    }
    // testing 결과 성공 

    이 코드르 생성자를 생성해서 point.getX() 가 1234가 나오도록 기대해보자 결과는 0으로 된다. 반대로 this. 를 붙혀 생성자를 작성하면 다음과 같이 testing을 무사 통과할 수 잇는걸 볼 수 있다.

    댓글

Designed by Tistory.