티스토리 뷰

1. this와 this() 키워드

 

JAVA에서의 this와 this() 키워드에 대해 알아봤다.

 

- this란?

: this란 객체 자신을 나타내는 참조 값을 의미한다. 클래스 내부에서 자기 자신(객체)을 가리키고 싶을 때 this 키워드를 이용한다. 주로 객체 내에서 자기 자신의 필드를 가리킬 때, 필드 값의 중복 방지를 위해 사용한다.

 

예를 들어 아래와 같은 코드가 있다고 했을 때

public class Person {
    private String name;
    private int age;
    
    public Person(String name, int age) {
    	this.name = name;
        this.age = age;
    }
}

 

아래 메인 함수에서 new 연산자를 사용해 객체를 Heap 영역에 할당하고 주소, 즉 참조값을 person 변수에 대입하는 코드가 있을 때 생성자 호출 시 this가 그 참조값을 뜻하게 된다. this는 참조 값을 의미하고, this.은 해당 객체를 나타낸다. 따라서 this.name을 하면 this가 가리키는 객체의 name필드를 의미하게 된다.

public class Main {
    public static void main(String[] args) {
        Person person = new Person("솝솝", 15);
    }
}

 

 

-this()란?

생성자 내부에서 다른 생성자를 호출할 때 사용된다. 주로 생성자 오버로딩 시 코드의 중복 방지를 위해 사용된다.

사용 예시는 아래 코드와 같다. 만약 아래와 같은 두 생성자 코드가 있을 경우, 

package org.example;

public class Person {
    private String name;
    private int age;
    private int phoneNumber;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public Person(String name, int age, int phoneNumber) {
        this.name = name;
        this.age = age;
        this.phoneNumber = phoneNumber;
    }
}

 

아래와 같이 this() 를 이용해 중복을 어느정도 줄일 수 있다.

package org.example;

public class Person {
    private String name;
    private int age;
    private int phoneNumber;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public Person(String name, int age, int phoneNumber) {
        this(name, age);
        this.phoneNumber = phoneNumber;
    }
}

 

*주의할 점: this()를 사용할 때는 this() 코드를 생성자 내부 맨 윗줄에 적어줘야한다. 안 그럼 컴파일 에러!!

 

2. Java의 Generic 타입

 

*generic: characteristic of or relating to a class or group of things; not specific.

 

*Java에서 Generic을 쓰는 경우를 예시로 들면 다음과 같다.

-> 마트에 가서 장바구니에 종류에 상관없이 제품을 담고 싶다

-> 근데 마트에 가서 직접 보고 골라서 장바구니에 담기 전까지는 내가 뭘 담을지 모른다

-> 장바구니에 들어갈 수 있는 물건에 제한을 두지 않고 싶다. 즉, 장바구니에는 뭐든지 들어갈 수 있음을 보장한다.

-> 예를 들어, 장바구니에 사과를 담을 수도, 생선을 담을 수도 있다.

 

이를 코드로 나타내봤다.

package org.example;

public class Basket<T> {
    private T content;

    public Basket(T content) {
        this.content = content;
    }
    public T getContent() {
        return content;
    }
}
package org.example;

public class Main {
    public static void main(String[] args) {
        Apple apple = new Apple();
        Fish fish = new Fish();

        Basket<Apple> basket1 = new Basket<>(apple);
        Basket<Fish> basket2 = new Basket<>(fish);

        System.out.println(basket1.getContent());
        System.out.println(basket2.getContent());
    }
}

 

위와 같이 Java에서 Generic 타입은 <>를 사용해 이용할 수 있다. Basekt 클래스의 Basket<T>부분이 제네릭을 사용하겠다는 부분이고, <> 안에 있는 T가 아직 결정되지 않은 파라미터 타입을 말한다. 즉 Basket 클래스는 아직 결정되지 않은, 보다 더 일반적인 (not specific) 제네릭 타입 클래스다. T 대신에 A가 들어가든 B가 들어가든 지지고 볶아도 노상관. 

 

위 처럼 아직 결정되지 않은 Basket 안에 있는 content의 타입은 main 메소드 내에서 생성자 호출을 통해 비로소 타입이 결정되고 초기화 된다. 타입은 컴파일 시점에 결정된다. 이를 통해 Basket은 좀 더 유연하고 확장 가능한 클래스가 될 수 있다. 

 

* 제네릭 메소드란?

-> 제네릭 타입을 사용하는 메소드

예를 들면 아래와 같음

package org.example;

public class BasketProvider {
    
    public static <T> Basket<T> createBasket(T t) {
        return new Basket<>(t);
    }
}

 

 

근데 Java에는 모든 클래스의 최상위 부모 클래스인 Object 클래스가 존재한다. 그렇다면 파라미터로 Object 타입을 선언하면 모든 문제가 해결되지 않을까?

 

-> 하지만 Object 클래스를 사용하면 그것이 무슨 타입인지, 명시적으로 확인하기 힘들다. 설령 알고 있더라도 그것을 사용하려면 강제 형변환을 통해 타입을 지정해줘야 한다. 

-> Generic을 쓰면 더 깔끔하게 처리 가능.  

 

3. final, static, static final 키워드

 

-final

final은 필드나 메서드, 클래스에 붙일 수 있다. 필드의 경우 생성자에서나 필드에서 초기값을 설정한 후, 프로그램 종료시 까지 해당 값의 변경을 금지하는 키워드다. 아래와 같이 사용

public class Computer {
    
    public final String corp = "samsung";
    public final int price = 1000;
    
}

만약 위와 같이 선언 되어 있을 때 corp나 price 값의 변경을 시도한다면 에러가 발생한다.

 

근데 만약 필드에 객체가 있다면 어떻게 될까?

public class Computer {

    public final int price = 1000;
    public final Corp corp = new Corp();
}
public class Corp {
    public String name;
}

 

final은 변수의 값을 변경하지 못하도록 막는다. 변수의 값에는 int 같은 원시 타입과 참조값이 들어갈 수 있다. 위 코드에서는 참조값을 final로 선언했기 때문에, 객체가 Corp에서 다른 것으로 바뀌는 것은 참조값이 달라지는 것이기 때문에 컴파일 에러가 발생하지만, corp.name = "뭐뭐뭐" 이런식으로 객체의 필드에 접근하는 것은 무방하다.

 

그렇다면 위에서 선언한 public final String corp 필드의 변경을 시도하면 컴파일 에러를 발생시키는 이유는 뭘까? 그건 Java의 String 클래스는 값의 변경을 시도하면 다른 메모리 공간에 새로운 문자열을 복사하여 세팅하기 때문에 참조값이 바뀌기 때문이다.  

 

클래스에 붙을 경우 상속이 금지된다. 즉 상속 관계가 해당 클래스에서 끝났음을 말한다.

public final class Computer {
    
    public String corp = "samsung";
    public int price = 1000;
    
}

 

 

메서드에 붙을 경우, 하위 클래스에서 오버라이딩을 금지한다.

public class Computer {
    
    public String corp = "samsung";
    public int price = 1000;
    
    public final void method() {
    	...
    }
}

 

-static

static 키워드를 사용하면 JVM의 런타임 데이터 영역(메모리)의 메소드 영역 중 static 영역에 데이터가 적재된다. 메소드 영역에 적재된 데이터는 프로그램 실행 종료 시 까지 존재한다는 특징이 있다. 메소드 영역에는 static 영역(static 변수, static 메소드)외에 클래스 정보/코드 등이 올라간다.

따라서 클래스 안에 있는 메소드를 '객체 생성 없이' 사용할 때에는 꼭 static 키워드를 사용해야한다. Heap에 적재되지 않았는데 메소드를 사용하려면 static 영역에서 가져오는 방법 밖에 없기 때문이다.

 

static 키워드를 사용하는 예로 '정적 팩토리 메소드'는 간단하게 아래와 같은 형식으로 되어 있다. createMember()에 접근하기 위해서는 코드  상으로 'Member.createMember()' 가 된다. 

package org.example;

public class Member {
    
    private String name;
    private int age;
    
    private Member(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    public static Member createMember(String name, int age) {
        return new Member(name, age);
    }
}

 

 

 

-static final

static과 final을 합쳐 놓은 것이다. 즉, 메소드 영역에 적재되는 정적 데이터이면서, 데이터의 변경을 금지하는 키워드이다. 

가령 절대 불변하는 값, 예를 들면 수학에서의 상수 값들은 이런 특징을 잘 나타내고 따라서 코드 작성시 static final 키워드를 이용할 필요성이 있다.

예를 들면, 어떤 수학 공식이 있을 때 원주율 파이는 불변하는 값이므로 아래와 같이 표현할 수 있다.

package org.example;

public class MathFormula {
    static final double PI = Math.PI;
    
    ...
}

 

4. super, super()

 

-super()란?

super()는 super()가 존재하는 클래스의 상위 클래스, 즉 부모 클래스의 기본 생성자를 호출하는 키워드이다.

 

어떤 클래스가 다른 클래스를 상속하는 경우, 자식 클래스의 객체를 생성할 경우 부모 클래스의 객체도 생성해줘야 한다. 그러므로 부모 클래스의 생성자도 호출되어야 하는데, 이때 super()가 그 역할을 해준다.

package org.example;

public class SuperConstruct {
    public static void main(String[] args) {
        class A {
            public A() {
                System.out.println("A 생성자 호출");
            }
        }

        class B extends A {
            public B() {
                super();
            }
        }
        B b = new B();
    }
}

 

위의 print문이 잘 실행된다. 위의 super()는 명시적으로 표시해줘도 되지만, 넣지 않아도 컴파일러가 알아서 자동으로 넣어준다. 그리고 부모 클래스의 매개변수가 존재하는 생성자를 호출하고 싶다면, 그 매개변수에 맞게 자식 생성자에서도 super(매개변수...) 처럼 정의해줘야 된다.

this()와 마찬가지로 super()는 코드블럭 맨 윗 줄에 정의해줘야된다.

 

-super란?

super는 상속 관계에서 부모 객체의 참조를 의미한다. 부모 클래스의 메소드나 필드를 자식 클래스에서 사용하고 싶을 때 사용할 수 있다. 아래는 부모 클래스의 메소드를 자식 클래스에서 사용하는 예시. 

 

package org.example;

public class Super2 {
    public static void main(String[] args) {
        class A {
            public void parentMethod() {
                System.out.println("A 클래스의 메소드");
            }
        }

        class B extends A {

            @Override
            public void parentMethod() {
                super.parentMethod(); // "A 클래스의 메소드"
                System.out.println("B 클래스의 메소드");
            }
        }

        B b = new B();
        b.parentMethod();
    }
}

 

 

5. SOLID 원칙

 

좋은 객체 지향 프로그래밍의 원칙이라고 불리는 다섯 가지 원칙인 SOLID 원칙

 

S: Single Responsibility Principle (단일 책임 원칙)

-> 단일 책임 원칙이란 하나의 클래스가 하나의 책임만 져야 한다는 뜻이다. 즉 하나의 기능만 담당해야 한다는 것. 

 

O: Open/Closed Principle (개방 폐쇄 원칙)

-> 소프트웨어의 요소는 확장에는 열려있어야 하지만 변경에는 닫혀 있어야 한다. 즉 코드를 추가해서 프로그램을 확장할 때 기존 코드의 수정을 최소화해서 개발해야 한다는 원칙을 말함. 유연한 소프트웨어와 제품 발전의 용이성을 위해 지켜야 할 원칙.

 

L: Liskov substitution Principle (리스코프 치환 원칙)

-> 프로그램의는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다. 즉 자식 타입을 언제든지 부모 타입으로 변경할 수 있어야 한다는 원칙이다. 

 

I: Interface segregation Principle (인터페이스 분리 원칙)

-> 특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다. 즉 인터페이스를 기능 별로, 즉 해당 인터페이스를 사용하는 클라이언트의 요구대로 잘게 나누는 것이 중요하다는 원칙. 

 

D: Dependency inversion principle (의존관계 역전 원칙)

-> 추상화에 의존하고, 구체화에 의존하지 말라. 즉 인터페이스나 추상 클래스에 의존하라는 말. 각 클래스간의 결합도를 낮추기 위한 원칙. 

 

6. 스프링의 의존성 주입 방식

 

-의존성이란?

의존성이란 뭘까? 두루뭉술해가지고 처음엔 이해가 잘 안됐지만, 내가 이해한 방식은 코드에서 의존성이란 어떤 코드가 다른 코드를 알고 있다, 포함하고 있다, 즉 해당 코드를 실행하기 위해서 다른 코드가 필요하다 정도로 이해했다. 코드로 표현하면,

package org.example;

public class Main {
    public static void main(String[] args) {
        class B{
        }
        
        class A {
            private final B b = new B();
        }
    }
}

 

위 코드에서 클래스 A는 클래스 B에 의존한다고 말할 수 있다. 하지만 위 코드는 SOLID 원칙에 위반된다.

 

첫 번 째로 개방폐쇄원칙에 위반된다. 만약 클래스 A가 의존하고 있는 B를 다른 클래스로 바꾸고 싶다면, A안에 있는 필드의 코드도 수정해야 하므로 OCP 위반이다.

 

두 번 째로 DIP 원칙 위반이다. 구현체에 의존하고 있기 때문이다. 따라서 추상화를 위한 인터페이스를 이용해 아래와 같이 바꿔봤다.

 

package org.example;

public class Main {
    public static void main(String[] args) {
        interface B{
        }

        class C implements B {
        }

        class D implements B{
        }

        class A {
            private final B b;

            A(B b) {
                this.b = b;
            }
        }
    }
}

 

 

그렇다면 C 객체나 D 객체와 같은 B의 구현체 중 A가 필요한 객체들을 어떻게 판별하고 어떻게 관리해야할까? 개방폐쇄원칙과 DIP원칙을 잘 지키면서 관리하기 위해선, A의 입장에서는 외부에서 필요한 객체를 주입해 주는 것이 더 나을 것이다. 즉, 외부에서 A는 C가 필요한지 D가 필요한지 정해준다. 이를 통해서 만약 의존관계 설정에 대해 코드의 수정이 일어난다면, 외부에서 정의한 해당 관계만 수정해주면 클래스 A에는 전혀 영향이 가지 않게 된다. 

 

-스프링의 의존성 주입

스프링의 DI 컨테이너가 위 역할을 대신해준다. 즉, 스프링이 런타임 시 의존 관계를 설정해준다. 이를 '제어의 역전' 이라고 한다.  DI 컨테이너는 위와 같은 의존 관계들이 기록되어 있는 일종의 관계도 라고 생각해도 될 것 같다. 

 

그렇다면 왜 스프링은 제어의 역전으로 객체들을 관리할까? 위에서 설명한 것처럼 개방폐쇄원칙과 DIP를 지키기 위해 사용된다. 이렇게 하면 클래스 A의 입장에서는 외부에서 주입 된 객체만 가지고 자기 자신의 역할을 수행하면 될 뿐, 그 외에는 신경 쓰지 않아도 된다. 이를 관심사의 분리 라고도 한다. 

 

DI 컨테이너가 의존성 주입을 하는 방식으로는 크게 다음과 같이 4가지가 있다. 스프링 DI 컨테이너에 등록된 Bean, 객체들을 필요한 곳 적재적소에 주입해준다. 아래 방식은 스프링 컨테이너에 Bean들이 잘 등록되었다는 가정 하에, 주입 방식에 대해서만 정리해보겠다. 

 

1) 생성자 주입 방식

package com.example.testspring;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class A {
    private final B b;

    @Autowired
    public A(B b) {
        this.b = b;
    }
}

 

- @Component: 해당 클래스를 스프링 빈으로 등록하겠다는 어노테이션(컴포넌트 스캔의 대상)

 

- @Autowired:  스프링의 이 기능을 사용하면 스프링 컨테이너에 등록된 인터페이스 B를 구현한 클래스의 객체를 이 클래스 A에 자동으로 주입해준다. 참고로 생성자가 단 하나 존재하면 @Autowired는 생략해도 된다. 

 

위 처럼 생성자를 이용해 주입하는 방식을 생성자 주입 방식이라고 한다. 

 

2) setter 주입

package com.example.testspring;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class A {
    private B b;

    @Autowired
    public void setB(B b) {
        this.b = b;
    }
}

 

-위 처럼 setter를 사용해 의존성을 주입한다. 생성자 주입 방식과 비교해보면, 생성자 주입 방식은 딱 한 번 실행되지만 setter 주입은 런타임 중 의존성 설정에 변경이 필요할 때 사용할 수 있다.  

 

3) 필드 주입

package com.example.testspring;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class A {

    @Autowired
    private B b;
}

 

필드 주입은 위처럼 필드에서 바로 주입한다. 

생성자나 어떠한 메소드도 사용하지 않고 의존관계를 설정하기 때문에 외부에서 변경하기가 불가능하다

-> 따라서 테스트를 하기 힘들고, DI 컨테이너에 너무 종속적이라는 단점이 있다.

 

그렇다면 가장 좋은 방식은?

: 생성자 주입

- 생성자 주입은 어플리케이션 실행 시 딱 한 번만 실행된다.

- 불변성을 보장하고, setter 주입이나 다른 메소드를 통해 의존성을 주입 받는 방식에 비해 안정적이다.

- 프로그래머의 실수를 줄일 수 있다.

- 다른 주입 방식과 달리 final 키워드를 이용할 수 있는데, final 키워드를 사용하면 생성자 실행 시점에 의존 설정이 안되어 있다면 컴파일 에러를 발생시켜 준다. 

 


7. Java Record 

 

참조: https://medium.com/@reetesh043/record-java-new-feature-daf97797bf3a

 

Record — Java New Feature

Java Records is a feature introduced as a preview feature in Java 14 and finalized later, to provide a concise and convenient way to…

medium.com

 

-Java Record란?

* record: 기록, 데이터로 다루어지는 단위

 

Java Record는 Java14에서 처음 소개된 기능이다.  Java Record를 사용하면 불변 객체를 다룰 때 캡슐화를 편리하게 할 수 있다고 한다. 필드, 접근 제어자, 메소드, final 키워드 등을 사용하여 불변 객체의 예시를 하나 만들어 보면 아래와 같다.

public final class Account {

    private final String name;
    private final int id;
    private final String type;

    public Account(String name, int id, String type) {
        this.name = name;
        this.id = id;
        this.type = type;
    }

    public String name() {
        return name;
    }

    public int id() {
        return id;
    }

    public String type() {
        return type;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Account that = (Account) o;
        return id == that.id && Objects.equals(name, that.name) && Objects.equals(type, that.type);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, id, type);
    }

   @Override
    public String toString() {
        return "Account{" +
                "name='" + name + '\'' +
                ", id=" + id +
                ", type='" + type + '\'' +
                '}';
    }
}

 

 

위 코드에선 클래스에 final을 사용하여 상속이 불가함을 나타냈고, 모든 필드에 final 포함, getter를 이용해 필드에 접근하도록 했다. 또한 equlas, hashCode, toString 같은 보일러플레이트(자주, 반복적으로 사용되는) 코드들을 오버라이딩 했다.

 

위 코드를 record 를 사용하면 아래와 같이 간결하게 바꿀 수 있다

public record Account(String name, int id, String type) {

}

 

 

record 클래스의 몇 가지 특징 정리

 

- 필드 캡슐화, 생성자 메서드, getter, equals(), hashcode(), toString() 컴파일 시점에 자동 생성.

- 클래스, 필드에 final 키워드 사용 -> 상속 불가, 데이터 불변성 보장

- 인스턴스 필드 선언불가 -> static 필드는 가능

- 인스턴스 메소드 가능 -> static 메소드도 가능

-  record 클래스는 절대 추상화 될 수 없음 (명시적인 final)

-  record 클래스는 인터페이스를 구현 할 수 있음

- new 키워드를 사용해 객체 생성 가능

 

record 클래스는 데이터의 전달에 유용하게 사용된다고 한다. DTO를 정의할 때 유용할 것 같다. 생각해보니 lombok의 상당 기능을 커버할 수 있을 것 같다.

'솝 키워드 과제' 카테고리의 다른 글

4차 세미나 키워드 과제  (0) 2024.05.08
3차 세미나 키워드 과제  (0) 2024.04.26
2차 세미나 키워드 과제  (2) 2024.04.07
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2026/03   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
글 보관함