반응형

1. 자바 제네릭의 한계?

자바 제네릭은 클래스에서 사용할 타입을 외부(사용부)에서 사용하게 해주는 일반적인 기법을 의미한다.

이러한 제네릭을 사용하면, 타입만 다르고 공통된 기능을 가지고 있는 클래스나 메서드들에 파라미터로 타입을 외부에서 전달받아 조금 더 범용적인 코드를 만들 수 있다. 이러한 제네릭이 없었다면, 자바 객체의 최고 조상인 Object 로 전달받아 객체 타입에 따라 캐스팅해 로직을 별도로 만들어야 되는데 이러면 오히려 런타임 시 예기치 못한 오류가 더 많이 발생하고 별도의 타입으로 만들어둔 클래스 및 함수들과 다를 바 없다.

 

public class Generic {
    public List<String> list = new ArrayList<>();

    public void addString(String str) {
        list.add(str);
    }

    public String getString(int index) {
        return list.get(index);
    }
}

// 제네릭이 아니라면, 이렇게 Object 로 전달
public class NonGeneric {
    public List list = new ArrayList();

    public void addString(Object obj) {
        list.add(obj);
    }

	// 원하는 타입으로 캐스팅 해 반환
    public String getString(int index) {
        return (String) list.get(index);
    }
}

 

그런데 자바 제네릭은 다른 언어의 제네릭과 달리 다른 점이 존재한다.

제네릭 도입 이후, 컴파일 타임 때까지는 타입 안전성을 얻을 수 있었지만, Type Erasure 기능으로 인해 컴파일 이후에는 타입이 소거가 되기 때문이다. 반쪽 제네릭이라고 불리우는 이유이다.

 

Class Test<T> {
	private T value;
    
    public Test(T t) {
    	this.value = t;
    }
    
    public T get() {
    	return value;
    }
}

// -> 컴파일 이후
Class Test {
	private Object value;
    
    public Test(Object t) {
    	this.value = t;
    }
    
    public Object get() {
    	return value;
    }
}

 

컴파일 타임 이후에 Object 로 변환이 된 것을 볼 수 있다. 컴파일 때까지만 타입 안전성을 제공하고, 그 이후에는 Object 로 변환이 되기 때문에 런타임 시에는 오류가 발생할 수도 있다.

 

자바 진영에서는 동세대 언어인 C# 과 다르게 보급률이 좋아서 현업 프로젝트들이 월등히 많았고 이러한 프로젝트들을 지키고자 하위 호완성을 위해 Type Erasure 를 도입했다고 한다. 컴파일 타임 때 Type Erasure 를 수행하면서 제네릭이 없던 하위 버전들과 동일한 형태로 class 파일을 생성할 수 있었다.

 

2. 타입 토큰의 등장

자바언어 개발자였던 Neal Gafter는 JAVA JDK5에 generics를 추가할 때 java.lang.Class 가 generic type이 되도록 변경했다고 한다. 예를 들어, String.class 의 Type 이 Class<String> 되도록 만들어 주었다. 그리고 이를 타입 토큰이라고 불렀다.

 

클래스 리터럴과 타입 토큰의 차이

클래스 리터럴은 어떤 클래스인지 파라미터로 전달되는 정보로 타입 토큰의 클래스 정보이다. 타입 토큰과 동일한 개념으로 타입 토큰은 클래스의 타입을 명시하고, 클래스 리터럴은 그 타입 토큰과 매칭되는 매개변수로 보면 된다.

 

// 선언부 (타입 토큰)
void myMethod(Class<?> clazz) {
	...
}
  
// 사용부 (클래스 리터럴)
myMethod(String.class);

 

이 타입 토큰은 컴파일 타임 이후에도 타입에 대한 안전성을 확보하고자 사용된다.

제네릭만 사용했을 때, Operation 이나 타입들의 메서드들이 동일해야 사용할 수 있다는 한계가 있긴 하지만 그래도 동일한 기능을 한 코드로 만들 수 있어 좋다. 그런데 원하는 타입으로 반환은 안 된다. 아까 위에서 언급한 것처럼 타입이 모두 Object 로 변환되기 때문에 타입 캐스팅이 되지 않는다. 이 때 타입 토큰이 유용하게 사용된다.

 

// 선언부
public <T> T readValue(String content, Class<T> valueType) {
	...
}

// 사용부
ProductDto productDto = objectMapper.readValue(jsonString, ProductDto.class);

 

위 예제처럼, valueType 으로 클래스 타입을 매개변수로 전달해주면 json 을 읽어 그 타입으로 반환해줄 수 있다.

조금 더 나아가 THC (Typesafe Heterogenous Container) Pattern 으로 사용될 수 있다.

 

// 선언부
public class SimpleTypeSafeMap {
	private Map<Class<?>, Object> map = new HashMap<>();
    public <T> void put(Class<T> k, T v) {
    	map.put(k, v);
    }
    public <T> T get(Class<T> clazz) {
    	return clazz.cast(map.get(clazz));
    }
}

// 사용부
simpleTypeSafeMap.put(String.class, "테스트");
simpleTypeSafeMap.put(Integer.class, 1);

String string1 = simpleTypeSafeMap.get(String.class);
Integer integer1 = simpleTypeSafeMap.get(Integer.class);

 

타입 토큰을 키로 사용해 타입끼리 저장하고 다시 그 타입 토큰으로 캐스팅해서 가져올 수 있다. 타입 안전 이종 컨테이너라고 불리우며 영어로는 THC (Typesafe Heterogenous Container) 라고 한다. 컨테이너가 타입을 체크해서 컴파일 여부를 결정하는 것이 아니라 타입 토큰을 가지고 프로그래머가 직접 핸들링하는 개념이다.

 

3. 슈퍼 타입 토큰의 등장

타입 토큰의 한계

이 타입 토큰에도 한계는 있다. 제네릭 타입의 클래스 리터럴과 타입 토큰은 존재하지 않는다. Type Erasure 때문에 <> 내부에 있는 타입들이 제거되기 때문이다. 조금 더 쉽게 생각해보면 이중 삼중... 다중 타입에 대한 정보들을 가지고 있지 않아서 그렇다. 이러한 타입 토큰의 한계를 극복한 개념이 슈퍼 타입 토큰이다.

 

슈퍼 타입 토큰

다중 타입에 대한 정보들을 리터럴로 표현할 수 있으면 타입 토큰처럼 안전성을 확보할 수 있을 것이다.

Class.getGenericSuperclass() 와 ParameterizedType.getActualTypeArguments() 를 사용해서 전체 타입에 대한 정보와 실제 타입을 가져올  수 있다.

 

class Super<T> {}
class MyClass extends Super<List<String>> {}

MyClass myClass = new MyClass();

Type typeOfGenericSuperclass = myClass.getClass().getGenericSuperclass();

// ~~~$1Super<java.util.List<java.lang.String>> 출력됨
System.out.println(typeOfGenericSuperclass);

 

Super 껍데기 클래스의 파라미터로 해당 타입을 전달하면 getGenericSuperclass 메서드로 전체 타입에 대한 정보를 가져올 수 있다. Class (Java Platform SE 8 ) (oracle.com)

 

Class (Java Platform SE 8 )

Determines if the specified Class object represents a primitive type. There are nine predefined Class objects to represent the eight primitive types and void. These are created by the Java Virtual Machine, and have the same names as the primitive types tha

docs.oracle.com

 

getGenericSuperclass 메서드에 대한 내용을 번역해보면 다음과 같다.

해당 객체의 상위 클래스 타입을 반환하는 메서드. 만약 상위 클래스가 매개변수화된 타입이라면 소스코드에서 사용된 실제 타입들을 반영해 반환해야 한다. 매개변수화된 타입에 대한 내용은 별도의 문서를 확인하라고 적혀 있다.(ParameterizedType은 제네릭 타입을 가지고 있는 타입을 말한다.)

ParameterizedType (Java Platform SE 8 ) (oracle.com)

 

ParameterizedType (Java Platform SE 8 )

ParameterizedType represents a parameterized type such as Collection<String>. A parameterized type is created the first time it is needed by a reflective method, as specified in this package. When a parameterized type p is created, the generic type declara

docs.oracle.com

 

제네릭 타입이라면, getActualTypeArguments 메서드를 통해 이 제네릭의 실제 타입을 가져올 수 있다.

 

class Super<T> {}
class MyClass extends Super<List<String>> {}

MyClass myClass = new MyClass();

Type typeOfGenericSuperclass = myClass.getClass().getGenericSuperclass();

// ~~~$1Super<java.util.List<java.lang.String>> 출력됨
System.out.println(typeOfGenericSuperclass);

// 수퍼 클래스가 ParameterizedType 이므로 ParameterizedType으로 캐스팅 가능
// ParameterizedType의 getActualTypeArguments()으로 실제 타입 파라미터의 정보를 구함
Type actualType = ((ParameterizedType) typeOfGenericSuperclass).getActualTypeArguments()[0];

// java.util.List<java.lang.String>가 출력됨
System.out.println(actualType);

 

슈퍼 타입 토큰 예제

// 선언부
public abstract class TypeReference<T> {
    private final Type type;

    protected TypeReference() {
    	Type superClassType = getClass().getGenericSuperclass();
        if (superClassType instanceof ParameterizedType) {
        	this.type = ((ParameterizedType)superClassType).getActualTypeArguments()[0];
        } else {
            throw new IllegalArgumentException("TypeReference는 항상 실제 타입 파라미터 정보가 있어야 합니다.");
        }
    }

    public Type getType() {
        return type;
    }
}

// 사용부
public class TypeSafeMap {
    private final Map<Type, Object> map = new HashMap<>();

    public <T> void put(TypeReference<T> k, T v) {
        map.put(k.getType(), v);
    }

    public <T> T get(TypeReference<T> k) {
        final Type type = k.getType();
        final Class<T> clazz;
        if (type instanceof ParameterizedType) {
            clazz = (Class<T>) ((ParameterizedType) type).getRawType();
        } else {
            clazz = (Class<T>) type;
        }
        return clazz.cast(map.get(type));
    }
}

 

Class.getGenericSuperclass() 와 ParameterizedType.getActualTypeArguments() 를 활용해서 실제 RawType 를 가져온다.

 

4. 프레임워크의 슈퍼 타입 토큰

이렇게 매번 직접 만들기에는 수고스럽다. 프레임워크에서도 이런 슈퍼 타입 토큰을 지원한다.

 

Spring 의  ParameterizedTypeReference

Spring 프레임워크에서도 동일하게 런타임 시 발생하는 타입 안정성 문제를 해결하기 위해 ParameterizedTypeReference라는 클래스를 만들었다. 매개변수화된 타입에 대한 정보를 가져올 때 사용하면 된다. 클래스 리터럴을 전달하는 대신 아래와 같이 사용하면 된다. new ParameterizedTypeReference<List<User>>()

 

spring-framework/ParameterizedTypeReference.java at main · spring-projects/spring-framework (github.com)

 

GitHub - spring-projects/spring-framework: Spring Framework

Spring Framework. Contribute to spring-projects/spring-framework development by creating an account on GitHub.

github.com

 

앞에서 살펴본 구현방식과 유사하다. 보통 다른 서버에서 자원을 가져올 때 사용한다고 한다.

ObjectMapper 에서는 ParameterizedTypeReference 를 사용하지 않고 직접 위에서 소개한 것처럼 구성하여 만들고, Spring Cloud feign 에서는 자체적으로 타입을 넘겨주지 않더라도 Reflection 기능을 통해 확인한다고 한다.

반응형

'JAVA' 카테고리의 다른 글

[JAVA] Stream Collector groupingBy  (0) 2023.02.26
반응형

커맨드 패턴은 "명령", "실행" 이라는 관점에서 DIP 원칙과 전략 패턴을 적극 활용한 패턴이다.

 

예시

선언부

public class Lamp {
	public void turnOn(){ System.out.println("Lamp On"); }
}
public class Button {
	private Lamp theLamp;
	public Button(Lamp theLamp) { this.theLamp = theLamp; }
	public void pressed() { theLamp.turnOn(); }
}

 

사용부

public class Main {
	public static void main(String[] args) {
 		Lamp lamp = new Lamp();
  		Button lampButton = new Button(lamp);
  		lampButton.pressed();
	}
}

 

Button 이 명령을 내리는 발행자로 명령을 받는 수행자 Lamp 와 강하게 결합이 되어 있다. 새로운 요구사항이 생겨 해당 Button 으로 다른 수행자의 명령을 수행하게 만든다면 어떻게 될까?

 

선언부

enum Mode { LAMP, ALARM };
public class Button {
	private Lamp theLamp;
    private Alarm theAlarm;
    private Mode theMode;
    
    public Button(Lamp theLamp, Alarm theAlarm) {
    	this.theLamp = theLamp;
        this.theAlarm = theAlarm;
    }
    
    public void setMode(Mode mode) { this.theMode = mode; }
    public void pressed() {
    	switch(theMode) {
        	case LAMP: theLamp.turnOn(); break;
            case ALARM: theAlarm.start(); break;
        }
    }
}

 

Button 에 명령을 수행하는 수행자 Lamp 와 Alarm 이 생성자 결합으로 강하게 묶이게 되고 계속 요구사항이 늘어날 때마다 Button 클래스를 수정해야 한다. 이는 OCP 원칙에 위배된다.

 

DIP 원칙을 이용하여 발행자와 수행자 사이에 객체를 하나 두어 한 단계 추상화해주면 된다. (인터페이스로 연결하고 각각의 구현체로 실질적인 로직들을 구현)

 

선언부

// 인터페이스
public interface Command { public abstract void execute(); }

public class Button {
	private Command theCommand;
	public Button(Command theCommand) { setCommand(theCommand); }
	public void setCommand(Command newCommand) { this.theCommand = newCommand; }
	public void pressed() { theCommand.execute(); }
}

public class Lamp {
	public void turnOn(){ System.out.println("Lamp On"); }
}
public class LampOnCommand implements Command {
	private Lamp theLamp;
	public LampOnCommand(Lamp theLamp) { this.theLamp = theLamp; }
	public void execute() { theLamp.turnOn(); }
}

public class Alarm {
	public void start(){ System.out.println("Alarming"); }
}
public class AlarmStartCommand implements Command {
	private Alarm theAlarm;
	public AlarmStartCommand(Alarm theAlarm) { this.theAlarm = theAlarm; }
	public void execute() { theAlarm.start(); }
}

 

사용부

public class Main {
	public static void main(String[] args) {
    	Lamp lamp = new Lamp();
        Command lampOnCommand = new LampOnCommand(lamp);
        Alarm alarm = new Alarm();
        Command alarmStartCommand = new AlarmStartCommand(alarm);
        
        Button button1 = new Button(lampOnCommand);
        button1.pressed();

		Button button2 = new Button(alarmStartCommand);
  		button2.pressed();
  		button2.setCommand(lampOnCommand);
  		button2.pressed();
	}
}

 

Button 에 한 단계 추상화 한 Command 를 부착할 수 있게 만들어 두고, 그 Command 의 메서드로 각각의 구현체가 동작하는 방식이다.

 

리팩토링 관점에서 보면...

사실 처음부터 설계하는 것보다는 기존의 코드를 수정할 일이 더 많다.

 

"명령" 과 "수행" 이라는 관점에서 커맨드 패턴을 떠올려 리팩토링할 수도 있지만, 조금 더 범용적인 관점에서 문제코드의 문제점을 해결할 수 있어야 커맨드 패턴을 처음 만든 프로그래머들의 취지를 이해할 수 있다고 생각한다.

 

커맨드 패턴을 알지 못 한 시점에서 해당 코드를 보고 무슨 문제점이 있는지 다시 살펴보자.

 

조건문(if 문이나 switch 문)에 각각 객체들이 포진되어 있고, 요구사항이 늘어날 때마다 결합되는 객체의 갯수와 조건문이 점점 커진다. 이 말은 고차원에서 저차원으로 가는데 저차원의 모듈 갯수가 현저히 많아 생긴 문제이다. 앞서 소개한 커맨드 패턴으로 예를 들면, 명령을 할 때 버튼을 눌러서 명령할 수도 있고, 손가락을 지시하여 명령할 수도 있다. 명령을 받는 주체는 그 명령을 받아 명령을 수행한다.

 

명령한다 => 버튼을 눌러서 명령한다 or 손가락을 지시하여 명령한다. => 램프를 킨다. or 알람을 설정한다. or 자동차 시동을 건다 등등

 

명령을 내리는 주체보다 명령을 받는 주체가 점점 많아지므로 DIP 원칙에서도 설명하였듯이 항상 저차원의 갯수가 많다. 따라서 고차원과 저차원의 모듈이 강하게 결합이 되는 것을 막으려면 한 단계를 추상화해 변수를 만들고 그 변수를 중간 매개체로 삼아 연결해야 한다. 그 매개체가 인터페이스이다. 이 인터페이스를 합성으로 고차원에 연결하면 앞서 소개한 커맨드 패턴이 되는 것이다.

 

결국 커맨드 패턴은 DIP 와 합성을 "명령" + "실행" 관점으로 바라본 디자인 패턴이다.

조건문이 계속 커지고 그 조건문 내에 객체의 갯수가 많아지면 인터페이스로 분리하여 합성하자.

반응형

'디자인 패턴' 카테고리의 다른 글

[디자인 패턴] 전략 패턴 (Strategy Pattern)  (0) 2022.04.23
반응형

1. 문제요약

코딩테스트 연습 - 풍선 터트리기 | 프로그래머스 스쿨 (programmers.co.kr)

 

프로그래머스

코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.

programmers.co.kr

일렬로 나열된 n 개의 풍선이 있다. 풍선들을 아래 규칙에 따라 단 1개만 남을 때까지 하나씩 터트린다고 할 때 최후까지 남기는 것이 가능한 풍선들의 갯수를 구하시오.

 

  1. 임의의 인접한 두 풍선을 고른 뒤, 두 풍선 중 하나를 터트린다.
  2. 터진 풍선으로 인해 풍선들 사이에 빈 공간이 생겼다면, 빈 공간이 없도록 풍선들을 중앙으로 밀착시킨다.
  3. 번호가 더 작은 풍선을 터트리는 행위는 한 번만 수행할 수 있다.

 

2. 문제예제

[9,-1,-5] 풍선이 주어질 때 다음과 같다.

 

  • 첫 번째 풍선(9가 써진 풍선)을 최후까지 남기는 방법은 다음과 같습니다.
    1. [9, -1, -5] 에서 -1, -5가 써진 풍선을 고른 뒤, -1이 써진 풍선(번호가 더 큰 것)을 터트립니다.
    2. [9, -5] 에서 9, -5가 써진 풍선을 고른 뒤, -5가 써진 풍선(번호가 더 작은 것)을 터트립니다.
  • 두 번째 풍선(-1이 써진 풍선)을 최후까지 남기는 방법은 다음과 같습니다.
    1. [9, -1, -5] 에서 9, -1이 써진 풍선을 고른 뒤, 9가 써진 풍선(번호가 더 큰 것)을 터트립니다.
    2. [-1, -5] 에서 -1, -5가 써진 풍선을 고른 뒤, -5가 써진 풍선(번호가 더 작은 것)을 터트립니다.
  • 세 번째 풍선(-5가 써진 풍선)을 최후까지 남기는 방법은 다음과 같습니다.
    1. [9, -1, -5] 에서 9, -1이 써진 풍선을 고른 뒤, 9가 써진 풍선(번호가 더 큰 것)을 터트립니다.
    2. [-1, -5] 에서 -1, -5가 써진 풍선을 고른 뒤, -1이 써진 풍선(번호가 더 큰 것)을 터트립니다.
  • 3개의 풍선이 최후까지 남을 수 있으므로, 3을 return 해야 합니다.

 

3. 팩트추출

Fact 1 : 숫자가 낮은 풍선을 터트리는 행위는 한 번만 할 수 있으므로 모든 조합을 탐색할 필요는 없다. 탐색 방향에 맞춰 조합을 쪼개 규칙성을 찾는다. 일부 조합의 결과를 찾는 알고리즘과 전체 조합의 결과를 찾는 알고리즘이 같다면 DP 이다.

 

일단, 풍선의 갯수를 n 이라고 가정하고 n 이 점차 증가했을 때 규칙성을 찾아본다.

 

1) n 이 1 일 때,

무조건 풍선이 남아 있으므로 1 반환

 

2) n 이 2 일 때,

1번 풍선이 작고 2 번 풍선이 큰 경우와 1번 풍선이 크고 2번 풍선이 작은 경우로 나눌 수 있다.

낮은 풍선을 터트리는 찬스 없이도 큰 걸 제거할 수 있으므로 2 반환

 

3) n 이 3 일 때, 가운데 풍선을 기준으로 4 가지 경우의 수로 나눌 수 있다.

 

3-1) 왼쪽이 작고 오른쪽이 큰 경우

1 2 3

1번을 제거하고 3번을 제거하면 (작은 것 => 큰 것 순) 2 생존

2번을 제거하고 1번을 제거하면 (큰 것 => 작은 것 순) 3 생존

2번을 제거하고 3번을 제거하면 (큰 것 => 큰 것 순) 1 생존

 

3-2) 왼쪽이 크고 오른쪽이 작은 경우

3 2 1

1번을 제거하고 3번을 제거하면 (작은 것 => 큰 것 순) 2 생존

2번을 제거하고 1번을 제거하면 (큰 것 => 작은 것 순) 3 생존

2번을 제거하고 3번을 제거하면 (큰 것 => 큰 것 순) 1 생존

= 3-1 경우의 수와 같다.

 

3-3) 왼쪽도 작고 오른쪽도 작은 경우

1 3 2

1번을 제거하고 3번을 제거하면 (작은 것 => 큰 것 순) 2 생존

3번을 제거하고 2번을 제거하면 (큰 것 => 큰 것 순) 1 생존

3번을 제거하고 1번을 제거하면 (큰 것 => 작은 것 순) 2 생존

2번을 제거하고 3번을 제거하면 (작은 것 => 큰 것 순) 1 생존

= 모든 경우의 수를 살펴봐도 중간값은 생존이 되지 않는다.

 

3-4) 왼쪽도 크고 오른쪽도 큰 경우

2 1 3

2번을 제거하고 3번을 제거하면 (큰 것 => 큰 것 순) 1 생존

2번을 제거하고 1번을 제거하면 (큰 것 => 작은 것 순) 3 생존

3번을 제거하고 1번을 제거하면 (큰 것 => 작은 것 순) 2 생존

 

현재 값보다 양 옆에 있는 숫자가 모두 작은 경우에만 생존하지 못 한다.

 

Fact 2 : 이전 결과 값이 그 다음에 영향을 미치지 않고 작은 문제들의 정답이 전체 문제의 정답이 되는 규칙성은 발견되지 않았다. (거의 모든 경우의 숫자가 살아 남으므로) 따라서 DP 나 분할 정복 문제는 아니다. 그러나 생존하지 못 하는 경우의 수로 보면 두 구역 모두 현재 숫자보다 작을 때 한 가지 경우의 수 밖에 없다. 따라서 경우의 수가 적은 여집합을 구한다.

 

Fact 3 : n 이 4일 때, 5일 때도 같은 규칙성이 존재하는지 확인한다. 작은 풍선을 제거하는 규칙을 사용하지 않고 큰 값만 골라 왼쪽, 오른쪽에서 제거한다면 항상 작은 값들만 배치하게 된다. 그렇게 n 이 3일 때로 만들고 위 규칙을 적용하면 결과값이 동일하다. 구역을 확장해도 해당 규칙에는 문제가 없다.

 

4. 문제전략

부분 문제로 보이지 않고 규칙성이 보이지 않으므로 완전 조합 문제로 풀어야 한다. 하지만 생존을 못 하는 여집합 경우의 수는 한 가지이므로 이 점을 고려하여 두 구역의 최솟 값보다 큰 경우의 숫자만 찾아 제외시켜 문제를 풀면 된다.

 

5. 소스코드

class Solution {
    public int solution(int[] a) {
        int count = 0;
        if (a.length == 1) return 1;
        if (a.length == 2) return 2;

        int leftMin = a[0];
        int rightMin[] = new int[a.length];
        rightMin[a.length-1] = a[a.length-1];

        for (int i = a.length - 2; i > 0; i--)
            rightMin[i] = Math.min(rightMin[i + 1], a[i]);

        for (int i = 0; i < a.length; i++) {
            if (!(leftMin < a[i] && rightMin[i] < a[i]))
                count++;
            leftMin = Math.min(leftMin, a[i]);
        }
        return count;
    }
}
반응형
반응형

[문제]

 

Cannot find Module 'X' Error in TypeScript

 

[원인]

타입스크립트에서 해당 모듈을 못 찾아서 오류가 발생하는 것이다.

 

[해결방법]

해당 모듈이 없는지, 해당 모듈이 자바스크립트로 되어 있는데 타입을 못 받아오는 건지, 경로를 못 읽는 것인지 여부를 살펴보아야 한다. tsconfig.json 타입스크립트 설정, webpack.config.js 웹팩 설정 등 종합적으로 살펴본다.

 

가장 먼저, 텍스트 에디터에서 파일 수정이 정상적으로 반영되었는지 살펴본다. 파일 수정이 정상적으로 반영되지도 않았는데 문제 해결할려고 시도하는 것만큼 미련한 짓은 없다.

 

필자가 그랬다... 1시간을 버렸다. Visual Studio Code 는 Intellij 와 다르게 빌드 시 자동으로 반영되지 않으므로 항상 저장이 되었는지 확인한다.

 

1. 해당 모듈 설치 확인

해당 모듈이 설치되어 있는지 확인한다. 간혹 NPM 으로 설치한 모듈들이 node.js 에서 사용하는 라이브러리들을 사용하곤 하는데 webpack 에서 node.js 의 핵심 모듈들을 제외시켜 직접 설치해야 한다.  [React] Reference Error: Buffer is not defined :: 골드에그 (tistory.com) 참고 바란다.

 

2. 타입스크립트 모듈 분석 확인

tsconfig.json 에서 moduleResolution 속성이 node 인지 체크한다.

{
  "compilerOptions": {
//+ "moduleResolution": "node",
  }
}

TypeScript: Documentation - Module Resolution (typescriptlang.org) 페이지를 살펴보면 모듈을 해석하는 방법이 Classic, Node 두 가지 방법이 있다고 소개하고 있다. NPM 으로 설치한 모듈들은 따로 상대경로를 주지 않으므로 Node 로 바꾸어야 한다.

 

3. 타입스크립트 include 설정 확인

tsconfig.json 에서 include 속성에 해당 모듈이 포함되어 있는지 확인한다.

{
  "compilerOptions": {
    // ...
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "src/**/*.spec.ts"]
}

include src/**/* 경로가 해당 모듈을 포함하고 있는지 확인해야 하며 포함되지 않는다면 추가해야 한다.

node_moudles 폴더는 제외한다.

반응형
반응형

1. 문제요약

코딩테스트 연습 - 다단계 칫솔 판매 | 프로그래머스 스쿨 (programmers.co.kr)

 

프로그래머스

코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.

programmers.co.kr

이 회사는 다단계 판매 업무를 하고 있다. 초대 받은 사람의 이익 10 퍼센트를 초대한 사람이 가지는 시스템인데 중앙에 있는 Center 까지 거슬러 올라가 모두 이익금을 나누어 가져야 한다.

 

판매원의 이름을 담은 배열 enroll, 각 판매원을 다단계 조직에 참여시킨 다른 판매원의 이름을 담은 배열 referral, 판매량 집계 데이터의 판매원 이름을 나열한 배열 seller, 판매량 집계 데이터의 판매 수량을 나열한 배열 amount가 매개변수로 주어질 때, 각 판매원이 득한 이익금을 나열한 배열을 구하시오.

 

2. 문제예제

emily 가 450 원을 벌었다면, mary 는 10 퍼센트인 45 원을 가지게 되고, center 는 45원의 10 퍼센트인 40원을 가지게 된다.

 

3. 팩트추출

Fact 1 : 어렴풋이 보면 트리 자료구조 문제로 보일 수 있으나 enroll 입장에서 보면 부모가 하나이기 때문에 항상 일차원적인 구조이다. 따라서 부모노드까지 계속 재귀호출하면서 이익금을 계산해주면 된다. enroll 의 갯수만큼 반복문을 수행하고 이익금을 나누어 가지면 된다.

 

4. 문제전략

트리 문제라고 속지만 않으면 된다. enroll 을 순회하면서 이익금을 계산하면 된다.

 

5. 소스코드

import java.util.*;

class Solution {
   static class Person {
        String name;
        Person parent;
        int sellAmount;

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

        public void calculateMultiLevel(int amount) {
            int parentAmount = amount / 10;
            this.sellAmount += amount - parentAmount;
            if (this.parent != null && parentAmount >= 1) {
                this.parent.calculateMultiLevel(parentAmount);
            }
        }
    }
    private static HashMap<String, Person> childParent;

    public int[] solution(String[] enroll, String[] referral, String[] seller, int[] amount) {
        childParent = new HashMap<>();
        for (String enrollP: enroll) {
            childParent.put(enrollP, new Person(enrollP, null, 0));
        }
        for (int i = 0; i < enroll.length; i++) {
            if (!referral[i].equals("-")) {
                childParent.get(enroll[i]).parent = childParent.get(referral[i]);
            }
        }
        for (int i = 0; i < seller.length ; i++) {
            childParent.get(seller[i]).calculateMultiLevel(amount[i] * 100);
        }
        int[] result = new int[enroll.length];

        for (int i = 0; i < result.length; i++) {
            result[i] = childParent.get(enroll[i]).sellAmount;
        }
        return result;
    }
}
반응형
반응형

1. 개요

의존 관계를 외부에서 주입(Dependecy Injection(DI))받는 것이 아닌, 직접 필요한 의존 관계를 찾는 것을 의존관계 조회(탐색) (Dependency Lookup(DL)) 이라고 한다. Spring 의 Application Context 전체를 주입받게 된다면, Spring Container 에 종속적인 코드가 되고, 그로 인해 단위 테스트도 어려워지게 된다. 이 때 필요한 것이 의존 관계 조회이다.

 

2. 본론

Spring 에서는 ObjectProvider 인터페이스로 DI Container 에서 해당 객체를 찾아 반환해준다.

DI Container 로 관리되는 Bean 객체들이 서로 의존성으로 엮여 있다면, 단위 테스트하기 많이 불편해지는데 해당 객체에서 필요한 객체들만 가지고 와 테스트하거나 의존성을 맺을 수 있므로 엄청난 이점을 가져다 준다. 사용방법은 단순하다.

 

ObjectProvider<찾고자 하는 클래스> testProvider = new ObjectProvider<>();

찾고자 하는 클래스 test = testProvider.getObject()

 

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Scope;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

import static org.assertj.core.api.AssertionsForClassTypes.assertThat;

public class SingletonWithPrototypeTest1 {

    @Test
    void prototypeFind() {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class);
        PrototypeBean prototypeBean1 = ac.getBean(PrototypeBean.class);
        prototypeBean1.getCount();
        assertThat(prototypeBean1.getCount()).isEqualTo(1);

        PrototypeBean prototypeBean2 = ac.getBean(PrototypeBean.class);
        prototypeBean2.getCount();
        assertThat(prototypeBean2.getCount()).isEqualTo(1);
    }

    @Test
    void singletonClientUsePrototype() {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(ClientBean.class, PrototypeBean.class);
        ClientBean clientBean1 = ac.getBean(ClientBean.class);
        int count1 = clientBean1.logic();
        assertThat(count1).isEqualTo(1);

        ClientBean clientBean2 = ac.getBean(ClientBean.class);
        int count2 = clientBean2.logic();
        assertThat(count2).isEqualTo(1);
    }

    @Scope("singleton")
    static class ClientBean {

        @Autowired
        private ObjectProvider<PrototypeBean> prototypeBeanProvider;

        public int logic() {
            PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
            prototypeBean.addCount();
            int count = prototypeBean.getCount();
            return count;
        }
    }

    @Scope("prototype")
    static class PrototypeBean {
        private int count = 0;

        public void addCount() {
            count++;
        }

        public int getCount() {
            return count;
        }

        @PostConstruct
                public void init() {
            System.out.println("PrototypeBean.init " + this);
        }

        @PreDestroy
        public void destory() {
            System.out.println("PrototypeBean.destory");
        }
    }
}

 

스프링의 ObjectProvider 말고도 JSR330 Provider 자바 표준 프로바이더도 있다.

implementation 'javax.inject:javax.inject:1' 만 의존성 추가해주면 사용이 가능하고 앞에 구문에서 ObjectProvider 를 Provider 로 수정하면 된다.

반응형
반응형

[문제]

 

 

[원인]

해당 타입은 Iterator 를 리턴하는 메서드가 필요하다고 한다. 그 말인 즉슨 타입이 배열같은 Iterator 를 내포하는 자료형이 아닌 자료형을 사용했다는 뜻.

 

[해결방법]

배열이 아닌데 ... 연산자를 사용했는지, 해당 자료형으로 for 문을 사용했는지 확인하고 바꾸자.

반응형
반응형

Spring 에서 Bean 으로 등록할 수 있는 @Bean, @Component, @Controller, @Service, @Repository 등 어노테이션들은 그 구현 클래스 내부에 변수를 생성해서는 안 된다. Bean 객체들은 기본 정책 상 싱글톤 객체이기 때문에 그렇다.

 

서로 다른 요청에 따라 쓰레드 별로 스택 메모리 영역을 차지한다고 하더라도, 이미 힙 영역에 Bean 객체가 로딩되어 그 로딩된 객체를 공유하기 때문에 상태 변수가 내부에 있다면 그 상태 변수가 의도하지 않은 상태로 실행이 될 수 있다.

 

따라서, Bean 객체는 메소드만 공유해야 하며, Bean 객체의 상태값을 유지하게 만드는 필드 값이 없는 무상태(Stateless)로 설계해야한다.

 

그런데 직접 설계해보니 결국 스프링은 대량의 요청을 받는 프레임워크이기 때문에 공유하는 오브젝트가 있을 수도 있다. 예를 들어 해당 요청의 서비스를 해야할지 말아야 할지 결정하는 전역변수 같은 경우이다. 되도록이면 사용하지 말아야 겠지만...

 

그 대신 레이스 컨디션이 되지 않도록 multi-thread 환경에 안전한 자료구조와 알고리즘을 사용하자.

 

참고

[스프링] 싱글톤 방식의 주의점 - 스프링 빈은 항상 무상태(stateless)로 설계하자 (tistory.com)

반응형

'JAVA > Spring' 카테고리의 다른 글

[Spring Boot] Auto-Configuration  (0) 2022.09.23

+ Recent posts