This 키워드란 ?
자바 내에서 객체의 멤버 변수를 관리할 때, 자신을 가리키는 인스턴스 변수를 표현하기 위해 this
키워드를 사용합니다.
public static Class Car{
String name;
String color;
public Car(String name, String color){
this.name = name;
this.color = color;
}
}
예시입니다. 자바 내에서 객체를 생성하기 위해서는 , 생성자를 통한 호출이 필요하고 호출 시에 설정할 멤버 변수를 인자로 넘겨주게 됩니다.
클래스는 붕어빵 틀과 같은 역할을 합니다. 어떤 붕어빵을 만들지는 사용자 입력에 따라 달라지게 됩니다.
하지만 인자로 받는 name 변수와, color 변수의 이름은 객체의 인스턴스 변수와 똑같습니다.
이 경우 무엇이 매개변수이고, 무엇이 클래스 인스턴스 변수인지 구분 할 수 없습니다.
따라서 this 키워드를 통해, 주입 받을 인스턴스 변수를 특정하고, 생성자로 부터 값을 주입받아 인스턴스를 생성하게 됩니다.
그럼 this() 는 뭐지 ?
this() 키워드는 괄호가 붙음에서 알 수 있듯이, 메서드 입니다.
그렇다면 어떤 역할을 수행하는 메서드일까요?
위 예시를 확장해보겠습니다.
public static Class Car{
String name;
String color;
String owner;
public Car(String name, String color, String owner){
this.name = name;
this.color = color;
this.owner = owner;
}
public Car(String owner) {
this("Genesis", "black", owner);
}
public Car(String color, String owner){
this("K5", color, owner);
}
}
객체의 멤버도 늘었고, 생성자의 개수도 늘어났습니다. 이제 this() 메서드가 등장하는데요,
기존 this 와 사용 방식이 조금 다릅니다.
기존 this 키워드는 생성자에서 현재 인스턴스 변수를 나타냅니다. 하지만 this() 메서드는 생성자를 호출합니다.
this() 키워드를 통해 고정된 인자들을 생성자로 전달하고, 변경할 부분만 전달하고 있습니다.
즉, this() 키워드는 같은 클래스의 다른 생성자를 호출하기 위해 사용합니다.
하지만 위와 같은 방식은, 정적 팩토리 메서드 방식에서 사용할 수 없습니다.
정적 팩토리 메서드는 인스턴스가 존재하지 않는 상황에서도 생성자 호출이 가능합니다.
this() 키워드는 현재 인스턴스를 참조하기 때문에 사용할 수 없습니다.
Generic 타입이란 ?
Generic 타입은 자바를 이용해 코드를 작성하면 반드시 접하게 되어있습니다.
하지만 그 뜻이 명확하지 않아, 이해하기 어려웠던 경험이 있습니다.
제네릭은 ‘범용’ 이라는 뜻을 가지고 있어, 정적 타입 언어인 Java, C, C++에 주로 사용되곤 합니다.
공식적인 설명은 위와 같습니다.
보통 Collection을 생성할 때,
HashMap<Integer, Integer> map = new HashMap<>();
이런 방식으로 생성하곤 합니다.
하지만 HashMap의 구현체로 들어갈 경우, 위와 같은 인자를 가지고 있는 것을 볼 수 있습니다.
이 때, K 와 V가 제네릭 프로그래밍에 사용되는 컨벤션 중 하나입니다.
클래스 외부에서 타입을 지정해 줌으로서, 유연한 프로그래밍이 가능하고, 코드의 재사용성이 높아집니다.
보통 아래와 같은 키워드를 컨벤션으로 사용합니다.
Type | Description |
<T> | Type |
<E> | Element |
<K> | Key |
<V> | Value |
<N> | Number |
사실 어떤 키워드를 사용하든지 상관은 없지만, 통일하는 것이 코드 가독성을 높일 수 있는 방법일 것입니다.
( 도 사용은 가능하지만, 설명이 필요하겠죠??)
제네릭 클래스 및 인터페이스 선언
public interface GenericInterface <T> {...}
public class GenericClass <T> {...}
제네릭 클래스에서 선언은 위와 같이 이뤄집니다.
Type 을 뜻하는 T는 { } 안에서만 영향을 줄 수 있습니다.
이제 HashMap의 구조를 이해할 수 있습니다.
HashMap class는 두개의 인자를 받습니다. 그 인자는 프로그램 실행 시점에 정해지고
사용자는 클래스 타입에 상관없이 Key, Value 지정이 가능합니다.
코드를 작성하는 개발자는 모든 타입에 대한 코드 작성 없이, 제네릭 형식을 사용하여 중복을 제거할 수 있습니다.
또한, 제네릭 프로그래밍은 프로그램 실행 시점에 해당 타입을 통한 객체 생성을 진행하는 방식이므로,
반드시 사용자가 타입을 명시해야 합니다.
public class HashMap<K,V> HashMap extends AbstractMap<K,V> {...}
public class Main{
public static void main(String[] args){
HashMap<Integer, String> map = new HashMap<>();
}
}
또한 제네릭 형식은 Reference Type 만 대체될 수 있습니다. 이는 int, char, double 와 같은 원시형(primitive) 타입은 대체될 수 없으며, Integer, String, Character 와 같은 Wrapper 객체를 통해서 사용이 가능합니다.
이 뜻은 곧 사용자가 임의로 생성한 Class 또한 제네릭 프로그래밍에 사용될 수 있음을 의미합니다.
메서드 레벨 제네릭
또한 제네릭은, 메소드 레벨에서도 사용이 가능합니다.
public <T> T genericMethod(T o){...}
[접근 제어자]<제네릭 타입>[반환 타입][메소드명]([제네릭 타입][파라미터])
와 같은 형태로 사용이 가능합니다.
HashMap의 valuesToArray 메서드 입니다. Generic 메소드의 형태를 띄고 있는 것을 확인할 수 있습니다.
타입 인자 배열을 매개변수로 받아, Hashmap의 Node를 순회하며 배열에 추가하고, 해당 형식으로 반환하는 모습을 볼 수 있습니다.
만약 제네릭 타입이 허용되지 않았다면 어떤 일이 벌어질까요 ?
아마 모든 자료형에 대한 메서드 구현이 필요할 것 같습니다.
class Something<E> {
private E element;
void set(E element){
this.element = element;
}
E get(){
return element;
}
static <T> T genericMethod2(T o) { //<--- 좀 이상한데
return o;
}
위와 같은 형식도 가능합니다. 조금 이상한 부분이 느껴질 수도 있습니다.
클래스에서 사용하는 제네릭 타입인 E와 별개로 T 타입을 메서드 반환값으로 사용하고 있습니다.
한 메서드에 다른 제네릭 타입이 등장하는데요, 왜 이런 형태를 사용하는 걸까요 ?
→ 바로 정적 메서드로 구현하기 위하여 사용하는 방식입니다.
제네릭 방식은 new 키워드를 통한 생성 시점에 타입이 지정됩니다.
하지만 정적 타입의 경우, 프로그램 실행 시점에 메모리에 올라갑니다.
그럼 어떤 타입을 받아올지 정해지지 않은 상태이기 때문에 타입 에러가 발생하게 됩니다.
위와 같은 상황을 위해, 클래스에 독립적인 제네릭을 사용하여 타입을 파라미터로 전송해 지정할 수 있습니다.
static <T> T genericMethod2(T o) { // 제네릭 메소드
return o;
}
<T> 와 (T o)는 엄연히 다른 제네릭 타입입니다.
System.out.println("<T> returnType : " + Something.genericMethod2(3.0).getClass().getName());
System.out.println("<T> returnType : " + Something.genericMethod2("ok").getClass().getName());
//Output:<T> returnType : java.lang.Double
//Output:<T> returnType : java.lang.String
위와 같은 구조 덕분에, static method 에서 인자로 전해지는 타입에 대해서 올바르게 반환이 가능합니다.
제네릭 범위 , 와일드카드
제네릭이 코드 작성에 유연함을 제공하긴 하지만, 모든 범위의 허용은 자칫하면
개발자의 의도와 벗어나는 코드가 될 수 있습니다.
이를 어떻게 방지할 수 있을까요 ?
자바에서는 extends
, super
, ?
로 이러한 현상을 방지하고 있습니다.
?는 와일드 카드로서, 제네릭 계의 조커라고 생각하면 되겠습니다.
<K extends T> // T를 상속하는 K만 가능 (T 포함)
<K super T> // T의 부모 타입인 K만 가능(T 포함)
<? extends T> // T를 상속하는 K만 가능 (T 포함)
<? super T> // T의 부모 타입인 K만 가능(T 포함)
<?> // 모든 타입 가능
와일드 카드는 스프링 부트를 이용한 개발에서 흔히 볼 수 있습니다. 바로 ResponseEntity<?>인데요,
위 뜻대로 해석한다면, 어떤 형태든 개의치 않고 ResponseEntity 형태로 감싸 반환한다고 볼 수 있습니다.
Final, static, static final 이란 ?
1. static
static 키워드는 ‘정적’ 의미를 가지며, 전역을 의미합니다.
객체 생성 없이 사용할 수 있는 필드와 메서드를 생성하고자 할 때 사용합니다.
2. final
final 키워드는 ‘최종’ 의미를 가지며, 한 번 할당된 값의 변경은 불가능 하다는 특징을 가지고 있습니다.
하지만 이 부분이 혼란을 야기하기도 하는데요, final 키워드는 재할당을 금지하는 것이지,
한번 할당된 값( 변수가 참조하는 객체 내부 필드 값)을 변경하는 것은 가능합니다.
즉 완전한 불변성을 보장하지는 않습니다.
final Person person = new Person("John", 30);
// person 변수는 재할당 불가능
person = new Person("Jane", 25); // 컴파일 에러 발생
// person이 참조하는 객체의 내부 필드 값 변경 가능
person.setName("Jane");
person.setAge(25);
System.out.println(person.getName()); // "Jane" 출력
System.out.println(person.getAge()); // 25 출력
3. static final
static final 키워드는 클래스 상수를 선언하는 데 사용합니다.
static 을 사용하지 않을 시, 인스턴스마다 상수를 일일히 메모리에 생성하게 되고, 오버헤드가 발생합니다.
static final 키워드를 사용 함으로써 재할당을 방지하고, 생성 시점부터 종료 시점까지 하나의 메모리를 통해
효율성을 올릴 수 있다고 생각하면 되겠습니다.
super 와 super()
this, this()와 비슷한 주제입니다. 키워드의 차이에서 알 수 있듯이, super는 변수, super()는 메서드입니다.
그렇다면 어떤 부분이 다른 걸까요 ?
자바에서는 상속을 이용해 코드를 작성합니다. 공통된 부분을 상속함으로서, 코드 재사용성을 올리고 객체지향 패러다임을 구현할 수 있습니다.
super 키워드는 상속된 객체가 부모 클래스에 가지고 있는 멤버 변수에 접근하기 위한 레퍼런스 변수입니다.
주로, 객체 안에 있는 부모의 멤버변수와 자신의 멤버변수를 구별하기 위해 사용됩니다.
하지만 super() 키워드는 자식 클래스의 생성자에서, 부모 클래스의 생성자를 호출하기 위하여 사용됩니다.
하지만, 몇가지 제약이 있습니다.
- super() 는 생성자 코드에서 사용될 때, 맨 앞줄에 사용되어야 합니다.
- 자식 클래스의 모든 생성자는 부모 클래스의 생성자를 포함하고 있어야 합니다.
class Animal {
protected String name;
public Animal(String name) {
this.name = name;
}
public void speak() {
System.out.println("I am an animal.");
}
}
class Dog extends Animal {
private String breed;
public Dog(String name, String breed) {
super(name); // 상위 클래스 생성자 호출
this.breed = breed;
}
@Override
public void speak() {
super.speak(); // 상위 클래스 메서드 호출
System.out.println("Woof!");
}
public void printInfo() {
System.out.println("Name: " + name);
System.out.println("Breed: " + breed);
}
}
public class Main {
public static void main(String[] args) {
Dog dog = new Dog("Buddy", "Golden Retriever");
dog.speak();
dog.printInfo();
}
}
다음은 예시입니다. super.speak()을 통해, 부모 클래스의 speak 메서드를 호출하는 모습을 볼 수 있습니다.
또한 Dog 클래스의 생성자에는 String name을 넘겨주어, 부모클래스의 생성자를 실행하고,
추가적인 breed 변수를 Dog 클래스 생성자에서 수정하는 모습을 볼 수 있습니다.
SOLID 원칙이란 ?
SOLID 원칙은 객체지향 설계에서 지켜야 할 5가지 원칙을 뜻합니다.
위 규칙들은 상호 연관되어 있는 주제입니다. 겹치거나 공유하고 있는 부분이 다수 존재하며,
반드시 따라야 하는 법칙이 아닌, best practice 라고 생각하면 됩니다.
Single Responsibility Principal (SRP)
단일 책임 원칙이라고도 불립니다.
각 객체는 하나의 책임만 담당해야 함을 뜻합니다.
회원 객체는 회원에 대한 정보만 관리해야 합니다.
그 외 다른 부가적인 기능에 개입하게 될 경우, 규모가 커지거나 요구사항의 변경 시 대응하기 어렵습니다.
바꿔 말하면, 단일 책임 원칙을 적용한 객체는 요구사항의 변경 시 , 해당 객체의 범위만 수정하면 되기 때문에
유지보수성이 올라간다고 생각해도 되겠습니다.
Open closed Principal (OCP)
개방 - 폐쇄 원칙은 확장에 열려있고, 변경에 닫혀있는 구조를 의미합니다.
잘 와닿지 않을 수 있지만, 추상화를 통한 객체 구축을 통해, 추가적인 기능 요청에
유연하게 대응할 수 있도록 하는 원칙입니다.
위와 같이 추상 인터페이스를 통한 구현을 진행한다면 , 추가 기능이 필요한 경우, interface 는 건드리지 않고 구현체를 수정함으로서 추가적인 기능 구현이 가능합니다.
Liscov substitution principal (LSP)
리스코프 치환 원칙은, 서브 타입은 언제나 부모 타입으로 교체할 수 있어야 한다는 원칙입니다.
→ 다형성의 원리를 이용하기 위한 법칙이며, 상위 타입으로 객체를 선언하여 하위 클래스의 인스턴스를 받으면, 업캐스팅된 상태에서 부모의 메서드를 사용해도 동작이 의도대로 흘러가야 하는 것을 의미합니다.
즉 부모 클래스의 행동 규약을 자식 클래스가 위반하면 안됨을 의미하는 원칙입니다.
class Animal {
int speed = 100;
int go(int distance) {
return speed * distance;
}
}
class Eagle extends Animal {
String go(int distance, boolean flying) {
if (flying)
return distance + "만큼 날아서 갔습니다.";
else
return distance + "만큼 걸어서 갔습니다.";
}
}
public class Main {
public static void main(String[] args) {
Animal eagle = new Eagle();
eagle.go(10, true);
}
}
출처: https://inpa.tistory.com/entry/OOP-💠-아주-쉽게-이해하는-LSP-리스코프-치환-원칙#lsp_원칙_위반_예제와_수정하기 [Inpa Dev 👨💻:티스토리]
위 animal을 상속받는 Eagle 에서, animal 의 go 를 오버라이딩 하여, 완전히 다른 메서드로 만들어버렸습니다.
이제 Animal eagle = new Eagle();
후 go 메서드를 실행하게 된다면, 전혀 다른 메서드이기 때문에 에러가 발생합니다.
이와 같이, LSP를 올바르게 지키기 위해서는 부모 메서드의 오버라이딩을 주의 깊게 진행해야 합니다.
Interface Segregation principal (ISP)
인터페이스 분리 원칙은, 각 인터페이스를 사용에 맞게끔 잘게 분리해야 한다는 설계 원칙입니다.
SRP ↔ ISP 는 굉장히 비슷해 보이지만, SRP는 클래스의 단일 책임, ISP는 인터페이스의 단일 책임을 강조하는 원칙입니다.
ISP 원칙을 통해 인터페이스를 적절하게 분리 함으로써, 인터페이스를 사용하는 클라이언트의
목적과 용도에 적합한 인터페이스 만을 제공하는 것을 목표로 합니다.
Dependency Inversion Principle(DIP)
의존성 역전 원칙은 어떤 클래스를 참조해서 사용해야 하는 상황이 생긴다면, 그 클래스를 참조하는 것이 아닌,
그 대상의 상위 요소를 참조하는 원칙입니다. 상위 요소는 인터페이스, 추상 클래스가 있겠습니다.
즉, 구현 클래스에 의존하지 말고, 인터페이스에 의존하라는 뜻입니다.
잘 와닿지 않겠지만, 우리는 일상 생활중에 DIP를 이미 적용하고 있습니다.
자바로 알고리즘 문제를 풀 때, Set 자료형을 사용하기 위해서
Set<Integer> set = new HashSet<>();
등을 선언합니다.
하지만 Set은 인터페이스고, 그 구현체는 아래와 같습니다.
즉, 우리는 이미 고수준의 인터페이스를 사용하며 DIP원칙을 익숙하게 사용하고 있습니다.
이렇게 구현체에 의존이 아닌, 인터페이스를 통한 의존을 통해 결합도를 낮추는 객체지향적 설계가 가능합니다.
스프링의 의존성 주입 방식
의존성 주입 (Dependency Injection)은 스프링 프레임워크의 핵심 개념 중 하나입니다.
의존성이 뭘까요 ?
의존대상 B가 변하면, 그것이 A에 영향을 미친다.
- 이일민, 토비의 스프링 3.1, 에이콘(2012), p113
B의 기능이 변경될 경우 A 또한 변경이 필요할 때, B의 변경을 A가 알고있어야 할 때를 ‘A와 B는 의존관계다’ 라고 말하곤 합니다.
스프링 부트는 이러한 의존 관계를 스프링에서는 제 3자를 통해 구현합니다.
즉, B 와 A 가 의존 관계임을, 제 3자인 스프링 컨테이너가 알려주는 방식입니다.
이러한 방식은 마치 A가 외부에서 B를 주입 받는 모습을 띄고 있기에 의존성 주입이라 불립니다.
위와 같은 의존성 주입 방식을 사용하게 된다면,
- 의존성이 줄어든다.
- 재사용성이 올라간다.
- 테스트하기 좋은 코드가 된다.
- 가독성이 높아진다.
와 같은 장점을 갖게 됩니다.
public class Store{
private Pencil pencil;
public Store(){
this.pencil = pencil;
}
}
위 코드는 뭐가 문제일까요 ?
바로 클래스 끼리 의존관계가 형성되어 있습니다. Store에서 Pencil이 아닌 다른 물품을 판매하게 된다면,
Store 내부 코드를 변경해야 합니다. 이는 유연함을 떨어뜨리고 객체 지향적이지 못한 코드 작성 방식입니다.
public interface Product{...}
public class Pencil implements Product{}
위와 같이 Product 인터페이스를 통해 Pencil을 구현한다면 조금 더 유연한 대처가 가능합니다.
또한 , Pencil을 필요로 하는 Store 객체에, Pencil의 주입이 필요합니다.
하지만 Pencil은 인터페이스를 통한 구현이 이루어졌기 때문에, 클래스 주입이 아닌, 인터페이스 주입이 가능합니다.
public class Store{
private Product product;
public Store(Product product){
this.product = product;
}
}
이 대목은 마치 외부에서 Product 를 주입하는 것과 비슷한 모습을 띄게 되고,
이러한 패턴을 통해 Product 의 구체 클래스에 Store가 의존하지 않아도 됩니다.
스프링 프레임워크는 이러한 부분을 대신하기 위해 DI 컨테이너를 지원하고,
런타임 시점에 필요한 객체를 Bean 으로 등록하는 방식을 통해 의존성 주입을 진행합니다.
스프링 프레임워크에서는 총 세가지 방법의 의존성 주입이 가능합니다.
1. 생성자 주입
먼저, 생성자 주입입니다.
@Service
public class SomeService{
private final SomethingRepository somethingRepository;
@Autowired
public SomeService(final SomethingRepository somethingRepository){
this.somethingRepository = somethingRepository;
}
위 방식은 생성자를 통해 의존관계를 주입받는 방식으로, 기존에 사용하던 주입 방식과 큰 차이는 없습니다.
하지만 생성자 주입은, 생성자의 호출 시점에 1회 호출이 보장되기 때문에 객체의 불변성을 보장하고, 주입을 강제할 수 있습니다.
그렇기 때문에 생성자가 하나만 존재할 경우 @Autowired 를 생략할 수 있습니다.
Spring 팀에서는 생성자 주입을 권장하고 있습니다.
2. Setter 주입
Setter를 통한 주입은 Setter를 이용해서 주입하는 방식입니다.
@Service
public class SomeService{
private SomethingRepository somethingRepository;
@Autowired
public setSomethingRepository(SomethingRepository somethingRepository){
this.somethingRepository = somethingRepository;
}
}
위 방식을 통해, 주입받는 객체가 변경될 가능성이 있을 경우 사용하기도 합니다.
하지만 거의 없다고 합니다.
3. 필드 주입
마지막으로 필드 주입 방식입니다.
@Service
public class SomeService{
@Autowired
private SomethingRepository somethingRepository;
}
// or
@RequiredAllArgsConstructor
@Service
public class SomeService{
@Autowired
private final SomethingRepository somethingRepository;
}
가장 간결함을 볼 수 있습니다. 하지만 필드 주입은 외부에서 접근이 불가능하다는 단점이 존재하기 때문에
테스트 코드 작성에 일부 불편함을 겪을 수 있습니다. 또한, 필드 주입은 DI프레임워크가 반드시 존재해야 하므로
프레임워크에 의존적인 방식입니다.
따라서 생성자 주입을 통한 DI가 가장 적절하다고 볼 수 있겠습니다.
Record란 ?
스프링 부트 프레임워크를 이용한 서버 개발에선, 불변 데이터를 전송하기 위한 객체로 DTO (Data Transfer Object) 를 사용합니다. Record는 자바 14부터 도입된 클래스 유형이며 DTO를 사용한 전달 작업을 간단하게 만들 수 있습니다.
Record의 특징으로는
- 멤버 변수는 private final로 선언된다.
- 필드별 getter가 자동으로 생성된다.
- 모든 멤버변수를 인자로 하는 public 생성자를 자동으로 생성한다.
- equals, hashcode, toString을 자동으로 생성한다.
- 기본 생성자는 제공하지 않으므로 필요한 경우 직접 생성해야 한다.
와 같은 특징이 있습니다. 한 눈에 봐도 많은 것들을 대신 처리해주고 있습니다. 따라서 단점도 존재합니다.
- 상속 불가
- 추상화 불가
- 인터페이스 구현 불가
와 같은 단점을 가지고 있지만, 기존에도 DTO는 값 전달을 위해서만 사용되기 때문에 큰 문제는 없을 것 같습니다.
record Person(
@NotBlank(message = "이름은 2글자 이상이어야 합니다 ")
String name,
int age
) {
...
}
위와 같이, 클래스 이름 뒤 괄호에 필드 목록을 지정하고, 필드 타입과 이름을 명시합니다.
이 외에 Validate , 등은 필드 목록에서 적용 가능합니다.
출처
'후기' 카테고리의 다른 글
[GDG] Google Developer Group x Whatever 4주차 회고 (0) | 2023.09.25 |
---|---|
[GDG] Google Developer Group x Whatever 3주차 회고 (0) | 2023.09.18 |
[GDG] Google Developer Group x Whatever 2주차 회고 (0) | 2023.09.10 |
[GDG] Google Developer Group x Whatever 1주차 회고 (0) | 2023.09.03 |