4 Week

11 분 소요




CQRS Pattern

Command and Query Responsibility Segregation (명령과 조회의 책임 분리)
즉, 명령(command)및 쿼리의 책임을 분리하는 패턴이다. 책임분리를 위해서는 코드의 모듈이 분리되어야 한다.
read 와 write를 분리하는것을 뜻하며 어플리케이션까지만 적용할 수도 있고, DB의 모델까지만 분리할 수도 있고, DB 그 자체를 분리하여 적용할 수도 있다.

  • 명령은 데이터중심이 아니라 작업 기반이어야 한다.
  • 명령은 동기적으로 처리되지 않고 비동기처리를 위해 큐에 배치될 수 있다.
  • 쿼리는 데이터베이스를 수정하지 않는다. 쿼리는 도메인 지식을 캡슐화하지 않는 DTO를 반환한다.


  • 장점
    • 읽기 모델과 쓰기 모델을 필요에 따라 독립적으로 확장 가능
    • 읽기 모델은 쿼리에 최적화된 스키마를 사용 가능
    • 호출되는 도메인 엔티티에 대해 확인하는 로직 구현이 더 쉬움
    • 보통 복잡한 비지니스 로직 구현은 대부분 쓰기 모델에 속하며, 읽기 모델은 간단하게 구현된다. 그에 따라 읽기와 쓰기를 분리하면 유지관리가 더 쉽고 유연한 모델이 구현될 수 있다.
    • DB에 논리적인 View가 아닌, Materialized View를 저장함으로써 애플리케이션에서 복잡한 조인이 사용된 쿼리문을 피할 수 있다.


  • 언제 쓰면 좋을까?
    • 많은 사용자가 동일한 테이터에 병렬로 엑세스 하는 공동작업 도메인일 경우
    • 한팀은 쓰기모델에 포함되는 복잡한 도메인 모델에 집중하고 다른팀은 읽기모델과 사용자 인터페이스에 집중할 수 있을 환경일 경우
    • 시스템이 시간이 자나면서 진화할 것으로예상되어 여러 버전의 모델을 포함할 수 있거나 비즈니스 규칙이 정기적으로 변하는 경우





Generic

아주 유용한 문법이고 대부분 최신 언어에선 사용한다.
타입시스템을 더 견고하게 사용하기 위해 제네릭을 사용한다.
제네릭은 형변환시 발생할 수 있는 문제들을 사전에 없앨 수 있다.
즉, 타입을 파라미터화 해서 컴파일 시 구체적인 타입이 결정되도록 해준다.

  • 파라미터 타입, 리턴 타입에 대한 정의를 클래스 내부가 아닌 외부에서 지정
  • 타입에 대해 유연성과 안정성을 확보한다.
  • 런타임 환경에 영향이 없는 컴파일 시점의 전처리 기술이다.
    • 타입을 유연하게 처리하며, 런타임에 발생할 수 있는 타입에러를 컴파일전에 검출한다.
public class CastingDTO<T> {
	private T Object;

	public void setObject(T Object) {
		this.object = object
	}

	public T getObject() {
		return object;
	}
}

// 형변환을 하지않고 편리하게 사용할 수 있다.
public void checkCastingDTO() {
	CastingDTO<String> dto1 = new CastingDTO<>();
	dto1.setObject(new String());

	CastingDTO<StringBuffer> dto2 = new CastingDTO<>();
	dto2.setObject(new StringBuffer());

	String test1 = dto1.getObject(); 
	StringBuffer test2 = dto2.getObject();
}

특징

  • 클래스 또는 메서드에 선언 가능
  • 동시에 여러타입 선언 가능
  • 와일드카드를 이용해 타입에 대해 유연한 처리 가능
  • 제네릭 선언 및 정의시 타입의 상속관계 지정 가능


컨벤션

제네릭 타입을 선언할 때 어느정도의 컨벤션이 존재한다.

  • E : Element (컬렉션에서 주로 사용됨)
  • K : Key
  • T : Type
  • N : Number
  • V : Value
  • ? : Wild Card
    • 모든 타입을 다 매개변수로 받을 수 있다.
      •  extends T : 상한 경계
      • ? super T : 하한 경계


타입범위지정

// T에 비교불가능한 타입이 온다면 이 메서드는 기능을 수행할 수 없다.
public int compare(T t1, T t2) { 
	double v1 = t1.doubleValue(); 
	double v2 = t2.doubleValue(); 
	return Double.compare(v1, v2); 
}

// Number를 상속받는 클래스만 올 수 있도록 지정할 수 있다.
public <T extends Number> int compare(T t1, T t2) { 
	double v1 = t1.doubleValue(); 
	double v2 = t2.doubleValue(); 
	return Double.compare(v1, v2); 
}
  • <? extends T> 와일드 카드의 상한 제한(upper bound) : T 타입과 T를 상속 받고 있는 타입
  • <? super T> 와일드 카드의 하한 제한(lower bound) : T 타입과 T의 상위 타입


제네릭 클래스

클래스 인스턴스화 시점에 제네릭 파라미터를 통해 타입 전달

class Sample<T> { 
	private T anonyTypeData; 
}


제네릭 메서드

메서드 호출 시점에 제네릭으로 리턴 타입, 파라미터의 타입이 정해지는 메서드

public class Student<T> { 
	static T getName(T name) { 
		return name; 
	} 
}

static 메서드는 제네릭을 사용할 수 없다.
인스턴스화 되기전에 메모리에 올라가야 하는데 타입이 결정되지 않았기 때문이다.


제네릭의 타입소거

제네릭의 primitive 타입 사용 불가
타입소거란 원소 타입을 컴파일 타임에만 검사하고 런타임에는 해당 타입 정보를 알 수 없는 것이다.
다른 말로는 컴파일 타임에만 타입에 대한 제약 조건을 적용하고, 런타임에는 타입에 대한 정보를 소거하는 것을 말한다.
제네릭에 primitive 타입을 사용하지 못하는 이유는 타입 소거와 관련이 있다.
제네릭 클래스는 타입 소거의 첫번째 규칙에 의해 타입 파라미터를 Object로 교체하는데 primitive 타입은
Object의 하위 타입이 아니기 때문에 제네릭에서 사용하는 것이 불가능하기 때문이다.

  • 자바 컴파일러의 타입소거규칙
    • 모든 타입 파라미터를 그들의 바운드나 Object 타입으로 교체한다.
    • 제네릭 타입을 제거한 후 타입이 일치하지 않으면 타입 캐스팅을 추가한다
    • 확장된(extended) 제네릭 타입의 다형성을 보존하기 위해 브릿지 메서드를 생성한다.


왜쓰는데?

특정 타입으로 종속받지 않아서 편리하다.

  • 재사용성 증가
    • 여러타입의 파라미터를 삽입할 수 있기 때문에 코드를 간결하게하고 재사용성을 높임
  • 컴파일시 타입에러 발견
    • 잘못된 타입이 들어오는걸 컴파일 단계에서 방지할 수 있음
    • 컴파일단계에서 타입에러를 발견할 수 있다.
      • 즉, 타입시스템을 더욱 견고하게 한다.
  • 컴파일러가 타입 변환 수행
    • 컴파일 단계에서 형변환을 해주기때문에 코드에서 형변환 불필요


타입시스템?

개발자가 올바른 프로그램을 작성하기 위해 도우미 역할을 하고 컴파일단계에서 타입을 체크하는 대충 뭐 그런거다. 불편하기 때문에 많은 개발자들이 정적타이핑 하는 자바를 버리고 동적타이핑 하는 파이썬, 루비 같은 스크립트 언어로 갈아타는 이유이기도 하다.
대표적인 프론트엔드, 백엔드 언어인 자바스크립트와 자바는 각각 약타입언어, 강타입언어 라고도 불린다.

왜 이런 차이가 발생할까?
더 고민을 해볼 주제이긴 하지만 일단 한가지 확실한 것은 자바는 보통 서버 언어로 쓰이는 경우가 많다.
강타입 언어인 자바를 서버 언어로 채택하는 이유는 바로 서버이기 때문이다.
생각해보자 런타임단계가 아닌 컴파일단계에서 강하게 체크를한다?
이건 결국 서버에서 고치기 보다 서버에 올라가기 전에 고치려고 하기 때문이다.
서버에 올라간것을 고치는 것보다 서버에 올라가기전에 고치는것이 훨씬 비용이 적게들기 때문에 최대한 빠른단계
즉, 컴파일단계에서 실수를 잡는게 좋기 때문이다.

ex) 디비에 올라간걸 고치기 힘들잖아… 그러니까 올라가기 전에 잡아야지





시간/공간 복잡도의 상관관계


  • 시간복잡도 : 특정크기의 연산에 걸리는 절대적인 시간 즉, 연산 횟수
  • 공간복잡도 : 특정크기의 연산에 드는 메모리 사용량


우리가 하는 거의 모든 작업들은 시간복잡도와 공간복잡도를 서로 교환 한다.
물론 이 둘이 모든 상황에서 정 반대에 있는건 아니다.


Caching

  • 시간복잡도의 리소스를 공간복잡도의 리소스로 바꾸는거다.
  • redis를 사용 해서 데이터를 보다 빠르게 조회할 수 있다면 그만큼 탐색범위를 줄여 시간복잡도를
    줄일 수 있지만 그만큼 캐싱에 들어가는 메모리를 사용해서 공간복잡도는 올라가게 된다.


HashMap

  • 해시알고리즘으로 만든 key를 메모리에 올려두고 그 key값만 찾아가면 되기때문에 데이터 크기에 상관 없이 시간복잡도가 O(1)인 대신 그만큼 key값을 저장하는 메모리 공간을 사용하기 때문에 공간복잡도는 올라간다.


만약 메모리가 부족하다면?

메모리를 적게 써야하는 상황이라면 공간복잡도를 줄여서 시간을 많이쓰는
즉, 탐색을 더해서 시간복잡도를 올려야 한다.


의문점

보통 공간복잡도 보다 시간복잡도를 더 우위에 두고 작업을 하기 마련이다.
그렇다면 그 이유는 과거에 비해 하드웨어의 발전이 많이 이루어 졌기 때문이고
웹 어플리케이션의 등장으로 request / response의 단순함으로 인해
메모리에 올려둔 객체들의 생명주기가 짧아져 가비지컬렉터에 의해 금방금방 지워지기 때문일까?


틀린 접근은 아니다. 세상이 발전함에 따라 과거보다 현재 시간복잡도가 더 중요하게 되었다.

  • 하드웨어의 폭발적인 발전
  • 멀티코어를 통한 동시성 개발
  • request / response 사이클로 인한 짧은 객체의 생명주기

하지만 우리가 접할 기회가 흔한건 아니지만
여전히 공간복잡도의 한계가 있는 경우가 있다.

  • 빅데이터
  • 백데이터의 크기
    • ex) 구글이 검색을 제공하기 위한 백데이터의 크기
  • 머신러닝이 학습하는 학습데이터는 거대한 메모리를 사용할까? 오랜 학습시간을 들일까?

명확하지 않은 결론을 내리자면,
우리는 현재는 시간복잡도를 더 우선해서 경감하는게 더 장점이 많은 시대에 살고있고
현업에서 어떠한 규칙으로 삼아도 될 정도이지만 몇십만명이 될지도 모르는 회원정보를
redis에 올리지 않는 것처럼 경우에 따라 적절하게 사용하는것이 중요하겠다.





To Do





Nested Class

중첩클래스(nested class)는 클래스 내에 정의된 클래스를 말한다.
중첩클래스는 static으로 선언되지 않은 내부클래스(inner class)와
static으로 선언된 정적 클래스(static class)로 나뉜다.

class OuterClass {
	...
	class NestedClass {
		...
	}
}


Inner Class

class OuterClass {
	private int a = 10;

	private class InnerClass {
		public void print() {
			System.out.println("OuterClass.a = " + a);
		}
	}
}
  • inner class를 인스턴스화 하려면 외부 클래스를 먼저 인스턴스화 해야 한다.
    • 외부클래스와 innerClass 두 객체의 참조값은 서로 다르다.
  • inner class는 자신을 둘러싼 외부 클래스의 인스턴스 변수 / 메서드에 접근할 수 있다.
    • 외부클래스 인스턴스에 대한 외부참조를 가지기 때문이다.
      • 따라서 가비지컬렉션이 수거하지못해 memory leak의 위험성이 존재한다.
  • 외부클래스에선 inner class 멤버를 사용할수 없다. (사용하려면 객체를 직접 발생시켜야함)
  • inner class는 외부클래스의 멤버와 동일한 이름을 사용할 경우 외부클래스 멤버에 접근하고 싶으면
    명시적으로 나타내야 한다. (OuterClass.this.a)
  • 외부클래스의 private 멤버에 접근할 수 있다.
  • inner class는 외부 클래스의 멤버이므로 접근제한자를 사용할 수 있다.
  • 정적 멤버선언(static)이 불가능하다.
    • final키워드를 사용하면 가능하다.
    • 자바 16 이후부터는 inner class에서 정적멤버 생성이 가능하다.


Static Inner Class

class OuterClass {
	...
	static class StaticInnerClass {
		...
	}
}
  • 같은 static inner class의 객체를 2개 만들어도 두 객체의 참조값은 서로 다르다.
  • 정적클래스는 외부 클래스를 인스턴스화 할 필요가 없기 때문에 외부클래스의 변수 / 메서드에 접근할 수 없다.
    (외부클래스의 static 멤버만 접근 가능)
  • 정적클래스 내부에서 static 멤버를 사용할 수 있다.


Local Class

class OuterClass {
    public void doSomething() {
    	class LocalClass { // 로컬 클래스
    		public void doSomething() {
    			// ...
    		}
    	}    	
    	LocalClass obj = new LocalClass();
    	obj.doSomething();
    }
}
  • 패키지나 클래스의 멤버가 아니므로 접근제어자 사용 불가
  • static 멤버 선언 불가
  • 객체 생성은 외부에서 불가능하고, 내부에서만 가능


익명 클래스

펑션을 상속받는? 이름이 없는 로컬클래스.
선언과 동시에 초기화가 이루어 진다.
이름이 없기 때문에 익명클래스는 객체를 여러번 생성할 수 없으며 생성자를 만들수도 없다.
클래스가 딱 한번만 필요할 때 (일회용) 유용하다.

class Ex {
	final int x = 10;
	
	Inner in = new Inner() {
		public void print() {
			System.out.println("overriding");
			System.out.println(x);
			System.out.println(y);
		}
		
		public void printX() {
			//익명 클래스 안에서 메서드를 생성하여 사용 가능
			System.out.println("Method 추가");
		}
	};
}
  • Outer Class의 지역변수가 final 로 선언되어야만 접근 가능
  • static 멤버 선언 불가
  • 멤버 인터페이스 선언 불가


장점


어느 메서드에서 부모 클래스의 자원을 상속받아 재정의하여 사용할 자식 클래스가 한번만 사용되고 버려질 자료형이면, 굳이 상단에 클래스를 정의하기보다는, 지역 변수처럼 익명 클래스로 정의하고 스택이 끝나면 삭제되도록 하는 것이 유지보수면에서나 프로그램 메모리면에서나 이점을 얻을 수 있다.

즉, 익명 클래스는 재사용할 필요가 없는 일회성 클래스를 굳이 클래스를 정의하고 생성하는 것이 비효율적이기 때문에, 익명 클래스를 통해 코드를 줄이는 일종의 기법이라고 말 할 수 있다.

다만, 익명 클래스 방식으로 선언한다면 오버라이딩 한 메서드 사용만 가능하고,
새로 정의한 메서드는 외부에서 사용이 불가능 하다.


활용

  • inner class는 참조값을 담아야 하기 때문에 인스턴스 생성시 시간,공간적으로 성능이 낮아진다.
  • static inner class는 외부 인스턴스에 대한 참조가 존재하기 때문에,
    가비지 컬렉션이 인스턴스 수거를 하지 못하여 memory leak이 생길 수 있다.
  • static inner class를 사용하는 것이 좋으며 static 키워드를 사용하고싶지 않으면 별개의 클래스로 만드는 것이 좋다.
  • 하지만 Lambda 의 경우 얘기가 조금 달라지는데…





람다

익명 함수 (Anonymous functions) 를 지칭하는 용어이다.
익명함수란 함수의 이름이 없는 함수이고 익명함수들은 공통적으로 일급객체 이다.

  • 일급객체 : 다른 객체들에 적용 가능한 연산을 모두 지원가는 객체.
    함수를 값으로도 사용할 수 있고 파라미터로 전달 및 변수에 대입도 가능하다.


Stream 연산들은 매개변수로 함수형 인터페이스를 받도록 되어있다.
그리고 람다식은 반환값으로 함수형 인터페이스를 반환하고 있다.
즉, 람다식이란 함수를 하나의 식으로 표현한 것이다.
함수를 람다식으로 표현하면 메서드의 이름이 필요 없기 때문에, 람다식은 익명 함수의 한 종류라고 볼 수 있다.


특징

  • 람다식 내에서 사용되는 지역변수는 final 키워드가 붙지않아도 상수로 간주된다.
    • 람다의 병렬처리가 Thread safety 한 이유이다.
  • 람다식으로 선언된 변수명은 다른 변수명과 중복될 수 없다.


장점

  • 지연연산수행 : 지연연산을 수행함으로써 불필요한 연산을 최소화 할 수 있다.
    • 지연연산이라는 단어 뜻이 헷갈리는데,
      메서드를 호출하여 사용하려면 클래스를 만들고 클래스를 인스턴스화 하여 해당 메서드에 접근해야 하는데 이 과정을 수행하기 위해선 클래스가 반드시 초기화 되어야 된다는 의미이다.
      여기서 람다식 을 사용하면 이런 과정 없이 필요한 순간에 1회용으로 익명함수를 정의하고 바로 호출하면 되므로 클래스의 초기화가 필요 없다는 의미이다.
  • 코드의 간결성 : 불필요한 반복문의 삭제가 가능하며 단순하게 표현할 수 있다.
  • 함수를 만드는 과정없이 한번에 처리할 수 있어 생산성이 높아진다.
  • 메서드를 변수처럼 다루는 것이 가능해진다.
  • 병렬처리 : 멀티스레드를 활용한 병렬처리에 안전하다.
    • Stream은 람다식을 사용해 별도의 스레드에서 병렬 처리를 관리한다.
    • 람다의 외부 참조변수가 항상 final이어야만 하는 이유이다.
    • 즉, 동시성 문제에 자유롭다.
    • 어플리케이션의 크기가 거대해 짐에 따라 멀티스레드 환경에서 데이터의 무결성을 보장하는,
      자바에서 함수형 프로그래밍인 람다 를 도입한 이유중 가장 큰 이유 중 하나 라고 생각한다.


단점

  • 람다를 사용하면서 만든 익명함수는 재사용이 불가능하다.
  • 디버깅이 어렵다.
  • 남발할 시 오히려 가독성이 떨어진다.
  • 람다의 Stream은 전통적인 for문에 비해 성능이 떨어진다.
    • 하지만 람다를 사용하는 이유는 이제는 성능상의 이점보다 데이터 무결성 이 더 중요해진 건 아닐까?


Stream

다양한 데이터를 표준화된 방법으로 다루기 위한 라이브러리 - Java8에 추가
Stream의 문법에 대해서는 이 글에서 다루지 않는다.

  • Stream의 특징
    • 데이터를 변경하지 않는다.
    • 1회용 이다.
    • 지연 연산을 수행한다.
    • 병렬 실행이 가능하다.


의문점

  • 람다는 일종의 inner class 인데 가비지컬렉터의 메모리 누수에는 안전하다 알아보자.

    자바에서 모든 메서드는 반드시 클래스 안에 소속되어 있어야 한다. 메서드를 만들기 위해서는 클래스를 만들어줘야 하고 클래스를 만들려면 생성자를 비롯한 멤버들도 구성해야 한다.
    그리고 이 메서드를 사용하려면 별도로 instance를 생성해서 이 instance를 통해 메서드를 호출해야 한다.
    하지만 람다로 일회용 메서드를 활용한다면 이 과정이 필요 없다.
    즉, 인스턴스화를 시키지 않기 때문에 람다는 외부클래스 인스턴스에 대한 외부참조가 없고
    따라서 가비지컬렉터가 수거하지 못하는 상황이 일어나지 않아 memory leak에 안전한 것이다.





Collection






중복에 관한 의문점


객체지향의 장점인 재사용성(상속, 다형성 등) 을 보면 중복은 좋지 않다?
유지보수 관점에서 중복을 하는것이 좋을지, 중복을 제거하는것이 좋을지 고려해야 한다.
멀티모듈의 최상위 모듈을 하나두고 설계하는 패턴의 장,단점과 관련이 있다고 한다.


중복이 나쁜경우

중복은 중복을 부르고, 논리의 조그마한 실수도 큰 오류가 날 가능성이 높아진다.
또한 어플리케이션이 복잡해 질수록 생산성은 떨어진다.
대부분의 경우 중복은 좋지 않다.


중복이 차라리 나은경우

자원의 효율적인 사용이나 중복으로 인한 문제 발생보다 시스템의 안정성을 위주로 할 경우
코드의 중복 보다는 인스턴스들의 중복이 포인트이다.
예를들면, 인스턴스를 여러개 둘 수 있지만, 그 인스턴스를 만드는 코드는 하나만 존재해야 한다.
이렇지 않을 경우 언제나 동기화를 시켜주는 문제가 발생할 수도 있다.
또한 중복코드가 서로 의존을 해서 순환참조가 발생할 때?

멀티모듈의 설계?

모듈은 패키지의 한단계 위의 집합체이며, 관련된 패키지와 리소스들을 재사용할 수 있는 그룹이라고 정의한다.


MSA프로젝트를 구성할 때 문제점


인증 서버, API서버, 배치서버 등을 각각 나누다 보니 기존 모놀리식에선 신경쓰지 못했던 문제중 하나가 중복코드의 문제이다.
ResponseEntityExceptionHandler 등의 모든 서버에서 쓰이는 로직은 서버 전부에서 코드를 가지고 있어야 했고, 변경되면 모두 다 바꿔줘야하는 비효율적인 문제가 발생한다.
IDE를 몇개씩 띄워놓고 서버를 돌리는것도 힘들기 때문에 멀티프로젝트로 구성되 있던 서비스를 멀티모듈로 전환하는 경우도 많이 있다.


멀티모듈


공통적인 기능을 모아 하나의 모듈(common)로 만들고 각 모듈에서 의존한다.
common 모듈에 공통로직을 몰아넣는다?
아니다. 어느 정도의 중복을 기준으로 common module 에 넣을 것이고, 어디까지의 역할만 시킬 것인지를 확실하게 나눠야 한다. 그렇지 않으면 common이 점 차 커지며 common안에 비즈니스가 흐르기 시작하고,
이것이 어쩔수 없이 반복되고 결국 다른 애플리케이션은 날씬하고 common만 굉장이 큰 프로젝트가 되버리는 지옥이 발생한다.


멀티모듈의 장점


  • 코드의 중복을 줄일 수 있다. 공통된 로직이 있는 여러 서비스를 운영할 때, 공통부분을 모듈화 하고 이 의존성을 추가하여 공유할 수 있다.
  • 각 모듈의 기능을 파악하기 쉽다. 공통의 기능은 의존송 주입으로, 모둘별로 기능을 분리하여 코드이해에 도움이 된다.
  • 독립된 jar 로 단독 실행 가능
    • 빌드도 쉬워진다.
  • 독립된 모듈(library)로서 여기저기서 가져다 쓸 수 있는 확장성
  • 모듈별로 사용하는 의존성 (버전포함)을 다르게 관리할 수 있음


주의할점


  • 공통 모듈이 가져야 하는 의존성을 고려해야 한다.
  • 특정 모듈에선 불필요한 의존성으로 인해 어플리케이션이 무거워 질 수 있다.
    • 모든 모든 모듈에서 전역적으로 적용되어야 할 때 common모듈에 넣자
  • 각 어플리케이션(모듈)에 대한 설정은 공통 모듈에 적용하지 않고 분리한다.





댓글남기기