반응형

 

개념

인프라 구축을 코드형 인프라(IaC) 로 구성할 수 있도록 괸리된 환경에서 배포해주는 서비스

  • 지정된 사용자가 원하는 서비스를 골라 프로비저닝
  • 해당 서비스에 다양한 Action 들까지 제공
  • CloudFormation 으로 구성
  • 내부적으로는 Systems Manager Automation 사용

구조

AWS Service Catalog 는 제품, 포트폴리오으로 구성되어 있다.

 

제품

  • 엔드유저에게 제공할 미리 구성된 AWS 인프라
  • 각 제품 별 버전 설정 가능
  • 신규 버전 생성 시 서비스 액션 역시 다시 연동 필요
  • CloudFormation Output 으로 엔드유저에게 정보 제

포트폴리오

  • 다양한 제품을 모은 관리 단위
  • 그룹/역할/사용자에게 해당 포트폴리오를 이용할 수 있는 권한 부여 가능
  • 다른 계정과 공유 가능
  • AWS Budget 생성 가능 (Tag 기반)

제약조건

  • 사용자가 제품을 프로비전할 때 사용하는 권한
  • 사용자가 권한을 가지고 있지 않더라도 제품을 생성하는데 필요한 권한 부여 가능
  • 반대로, 사용자가 충분한 권한을 가지고 있어도 제품을 생성 및 사용하는데 제약할 수 있는 권한 부여 가능
  • = > 즉 사용자의 권한을 가지고 프로비저닝을 하는 것이 아니라 제약 조건에 명시된 권한으로 프로비저닝
  • 서비스 액션 등에도 제약 조건 권한 사용 가능
  • IAM 정책 기반
  • 아래 유형의 권한으로 구성
    • 시작
    • 알림
    • 템플릿
    • StackSet
    • 태그 업데이트

서비스 액션

  • 앤드유저가 제품을 프로비전 후, 제품을 제한적으로 관리하는 방법
  • 각 액션 수행 후 이벤트 로그를 통해 수행 결과 확인 가능
  • 내부적으로는 Systems Manager Automation 사용

 

서비스 카탈로그 실습

1 . AWSServiceCatalogEndUserFullAccess 정책 가진  enduser 사용자를 생성한다.

(서비스 카탈로그에 접근 가능한 접근만 가지고 있다.)

2 . enduser 사용자가 사용할 제약조건 역할을 생성한다. 정책 생성 후 역할을 연결!

3 . 서비스 카탈로그에 제품 생성 후, 포트폴리오 생성한다.

4 . 해당 포트폴리오에 enduser 사용자를 추가하고 제약조건 역할을 연결한다.

 

인프라 관리자가 아니라면, 프로비저닝 하위 항목들만 표시된다. 인프라 관리자가 생성한 항목들만 확인할 수 있다.

 

삭제 시 주의해야 할 점

포트폴리오 삭제 시 제품과 포트폴리오에 연결된 주체/역할 부터 먼저 삭제해야 함

반응형

'인프라 > AWS' 카테고리의 다른 글

AWS DEA-C01  (0) 2025.01.27
AWS EBS & EFS  (0) 2024.09.03
AWS S3 정리  (6) 2024.09.01
AWS Step Functions  (0) 2024.08.20
Amazon EKS 역할 / 네트워크 / 볼륨 / 모니터링  (0) 2024.07.20
반응형

0 . IAM 계정에서 AdministratorAccess 권한 추가

IAM 권한 권한  페이지에서 AdministratorAccess 권한이 있는지 확인한다. 없으면 해당 역할을 추가한다.

 

1 . AWS Identity Center devops 용 사용자 생성

계정 생성 시 기입한 이메일로 확인해서 계정 비밀번호를 생성해야 한다.

 

IAM Identity Center > 설정 > 자격증명 소스에서 AWS 액세스 포탈 URL 을 기억해둔다.

 

2 . AWS CLI 로컬에 설치

AWS 공식 홈페이지에서 각 운영체제에 맞게 AWS CLI 를 설치할 수 있다.

https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html

 

Install or update to the latest version of the AWS CLI - AWS Command Line Interface

When updating from a previous version, the unzip command prompts to overwrite existing files. To skip these prompts, such as with script automation, use the -u update flag for unzip. This flag automatically updates existing files and creates new ones as ne

docs.aws.amazon.com

 

Mac OS 기준 CLI 설치 명령어는 다음과 같다.

curl "https://awscli.amazonaws.com/AWSCLIV2.pkg" -o "AWSCLIV2.pkg"
sudo installer -pkg AWSCLIV2.pkg -target /

which aws
aws --version

 

3 . AWS SSO 설정

aws configure sso 명령어로 아래 SSO 를 세팅한다.

 

SSO session name : SSO 세션 이름

SSO start URL : AWS 액세스 포탈 URL (1에서 기억한 URL)

SSO region : SSO 계정이 활동할 region

SSO registration scopes : SSO 활동 범위 (디폴트 선택해도 무방)

 

이후 웹 페이지에서 SSO 계정 로그인

 

CLI default client Region : AWS CLI 가 활동할 region (SSO region 동일하게 적용)

CLI default output format : text, json, table 중 하나 선택 (디폴트로 text 선택)

 

aws s3 ls --profile SSO 사용자 명령어 실행 후, 명령어가 정상적으로 실행되는지 확인한다.

 

4 . AWS_PROFILE 환경변수 세팅

export AWS_PROFILE=SSO 사용자 이름

 

5 . terraform init && terraform plan 명령어 실행

terraform 프로바이더에 계정에 관련된 access_key, secret_key 삭제 후 테스트

반응형
반응형

 

 

Amazon EKS 가 마스터 노드를 Amazon EKS Node groups 가 워커 노드를 관리해준다.

 

EKS 인증

 

 

1 . kubectl 명령을 내리면 Kubernetes Master Node 서버에 API 호출

Kubernetes API 서버 인증 토큰과 IAM 인증 토큰(Kubeconfig 파일) 토큰을 함께 보냄.

2 . 요청한 사용자가 IAM User 맞는지만 확인

별도로 Policy 정책은 확인하지 않음. aws-auth 라는 configmap 생성함. (Kubectl edit configmap aws-auth -n kube-system 명령어로 확인 가능)

 

 

Groups 에는 쿠버네티스 RBAC 권한 정보가 포함됨.

 

EKS 역할

 

영역으로 분류

특정 네임스페이스 리소스 사용 권한 설정과 클러스터 전역의 리소스 사용 권한 설정으로 나뉠 있다.

 

책임으로 분류

IAM Role 유사하게 리소스에 대한 권한이 명시적으로 기재되어 있는 Role 해당 Role 권한을 할당받을 사용자나 그룹을 설정하는 Role Binding 으로 이루어져 있다.

 

사용자 추가하고 싶을

1 . IAM Role 생성

정책들은 확인하지 않고 사용자인지만 확인하기 때문에 역할만 생성해도 된다.

2 . aws-auth Configmap mapRole 추가

 

 

3 . ClusterRole 또는 Role 생성

 

 

4 . ClusterRoleBinding 또는 RoleBinding 생성

 

 

5 . aws-auth Configmap mapRole group 설정

 

 

6 . 대상 사용자의 kubeconfig 파일에 인증 정보 설정

kubectl 명령어를 호출할 , 어떤 role 실행할지 명시를 해주어야 하기 때문에 설정이 필요하다.

 

 

EKS 네트워크 구조

 

 

단일 호스트일 쿠버네티스 네트워크는 eth0 물리 네트워크 인터페이스, docker0 브릿지 네트워크, veth0 가상 네트워크 인터페이스 구조로 구성되어 있다. Pod 안에 여러 개의 컨테이너를 생성하면 가상 네트워크 인터페이스를 공유하면서 하나의 IP 부여받게 된다. Pause 컨테이너를 통해 공유하게 된다.

 

참고로 도커에서는 --net=container:컨테이너이름 옵션을 주게되면 위와 같이 컨테이너 네트워크 공유를 있다.

 

 

다중 호스트일 때는 위와 같이 호스트 내부 IP 충돌하는 문제가 발생할 있다. 그래서 호스트가 여러 되어도 컨테이너를 찾아갈 있게 모든 호스트의 브릿지 네트워크에 각기 다른 IP 할당하는 Overay Network 구조를 생각하게 되었다.

쿠버네티스 코어에 있는 것은 아니고 weave net 이나 AWS CNI, calico, flannel 쿠버네티스 플러그인으로 제공한다. Amazon EKS AWS CNI 사용한다.

 

그런데 AWS CNI 다른 플러그인과 다르게 가상 IP 부여하는 것이 아닌 VPC IP Range 내부 IP 할당하게 된다.

 

 

DaemonSet IPAM 이라는 모듈에서 ENI 로부터 IP 받아 Pod 할당하게 되는데

ENI 할당할 있는 IP 수를 초과하면 추가로 ENI 생성해서 IP 부여한다.

 

인스턴스 유형마다 최대 ENI 개수와 ENI 할당할 있는 최대 IP 주소가 다르다.

Amazon EC2 인스턴스 유형 사양 - Amazon EC2 ((트워크 사양 참고)

 

따라서 최대로 띄울 있는 Pod = 인스턴스 유형에 따른 ENI * (ENI IP - 1) 개이다. (ENI 자체가 할당 받는 IP 주소가 있으므로)

 

EKS 볼륨 관리

 

Pod 내려가면 데이터가 날라가기 때문에 안정적인 데이터 관리가 필요하다.

심플하게 안정적인 저장소를 준비한 다음 컨테이너에 안정적인 저장소를 마운트하면 된다. EBS EFS 준비한다.

 

1 . Storage Class 생성

어떤 저장소를 사용할 것인지 명시한다.

 

 

2 . Persistent Volume Claim 생성

어떤 스펙으로 생성할 것인지 명시한다.

 

 

3 . 컨테이너에 마운트

 

 

EKS 모니터링

 

마스터 노드 로그는 대상 클러스터 로깅 설정으로 CloudWatch 로그가 적재되고 확인할 있다.

Pod 모니터링은 Container Insights 통해 확인할 있다.

1 . 워커 노드의 IAM Role 편집

2 . CloudWatchAgentServerPolicy 정책 추가

3 . 빠른 시작 설정

4 . CloudWatch 페이지 이동

5 . Container Insights 선택

6 . 대상 EKS 클러스터 선택

 

출처

쿠알못이 Amazon EKS로 안정적인 서비스 운영하기 - 최용호 (넥슨코리아) :: AWS Community Day 2020 (youtube.com)

반응형

'인프라 > AWS' 카테고리의 다른 글

AWS DEA-C01  (0) 2025.01.27
AWS EBS & EFS  (0) 2024.09.03
AWS S3 정리  (6) 2024.09.01
AWS Step Functions  (0) 2024.08.20
AWS Service Catalog  (0) 2024.08.19
반응형

쿠버네티스 정의

컨테이너를 도커 런타임에 올려서 관리 운영, 클러스터 서비스를 지원해주는 프레임워크.

 

쿠버네티스 구조

[ 요약 ]

 

 

[ 상세 ]

 

 

components -> api -> etcd 순으로 watch 하고 있다가 변경 사항이 발생하면 watch 대상에게 notify 한다.

구조에서 components API 서버와 Pod 제외한 나머지들 (Controller Manager, Scheduler, Kublet) API 서버에 있는 리소스들은 사실상 etcd 내부에 있는 내용들이다.

 

쿠버네티스 실행 흐름

(kubectl get events)

 

 

Deployment -> ReplicaSet -> Pod 순으로 실행된다.

7 pod 선정에 대한 내용 로직들은 다음과 같다.

 

 

스케줄러가 kubelet 에서 보내준 노드 / 컨테이너 지표(CPU, Memory 사용량), Affinity, Taint 설정 등을 종합적으로 분석하여 새로운 파드를 생성할 워크 노드를 선정한다.

 

쿠버네티스 고가용성

 

쿠버네티스 구조를 보면 나머지 구성 정보는 Pod 재빠르게 뜨면제가 없을 있지만, API 서버와 etcd 죽으면 가용성에 문제가 생긴다. 그래서 API 서버는 2, etcd 3대가 최소 필요하다. etcd RAFT 라는 분산합의 알고리즘으로 인해 홀수 개를 유지해야 해서 최소 3대가 필요하다.

 

출처

쿠알못이 Amazon EKS로 안정적인 서비스 운영하기 - 최용호 (넥슨코리아) :: AWS Community Day 2020 (youtube.com)

반응형
반응형

ITEM 44 "표준 함수형 인터페이스를 활용하라"

 

자바8 부터 람다를 지원하면서 템플릿 메서드 패턴보다는 전략을 함수 객체 인자 형태로 전달하는 템플릿 콜백 패턴(전략 패턴) 이 모범이 되었다. 

 

  • 템플릿 메서드 패턴
abstract class Car {
	// 공통 메서드
    public void drive() {
        System.out.println("운전 시작");
        moving();
        System.out.println("운전 완료");
    }
    
    // 변화하는 부분
    abstract void moving();
}

class Hyundai extends Car {
	@override
    public void moving() {
     	...
    }
}

 

상위 클래스의 변하는 부분들을 메서드로 분리해 하위 클래스에 재정의하는 기법을 템플릿 메서드 패턴이라고 한다. 상속을 사용한다.

 

  • 템플릿 콜백 패턴 (전략 패턴)
// 선언부
class Car {
	// 공통 메서드
    public void drive(MovingStrategy movingStrategy) {
        System.out.println("운전 시작");
        movingStrategy.moving();
        System.out.println("운전 완료");
    }
}

// 사용부
// MovingStrategy 인터페이스에는 moving 이라는 추상 메서드 하나만 존재해야 한다.
Car myCar = new Car();
myCar.drive(() -> System.out.println('100km/h 주행 중'));

 

이와는 반대로 함수 객체를 사용하는 시점(moving 함수 호출)에 전략 함수 객체를 인자로 받아 실행하는 형태를 템플릿 콜백 패턴. 즉 전략패턴이라고 한다. 함수 생성 시점에 전략 객체를 같이 생성하여 강하게 결합하는 것이 아닌, 사용하는 시점에 전략 객체를 인자로 받아 사용하고 제거하는 형태로 결합해 결합도가 낮아 유연하다는 장점이 있다.


 

LinkedHashMap 를 커스터마이징해서 더 자세히 알아보자.

LinkedHashMap 의 removeEldestEntry() 함수는 맵에 새로운 키를 추가할 때 호출되는 put() 메서드에 의해 호출되는데, 해당 메서드가 true 를 반환하면 맵에서 가장 오래된 원소를 제거한다. 이 removeEldestEntry 함수를 오버라이드해서 가장 최신 5 개인 데이터만 유지하는 캐시를 만들어보자.

 

람다 이전에는 아래와 같이 템플릿 메서드 형태로 직접 하위 클래스에서 오버라이딩했었다.

 

protected boolean removeEldestEntry(Map.Entry<K,V> eldest){
	return size() > 5;
}

 

removeEldestEntry 메서드는 인스턴스 메서드라서 바로 자신 Map 객체의 size() 호출을 통해 맵 안의 원소 수를 알아낼 수 있다. 이 부분을 함수형 인터페이스를 사용해서 전략 패턴으로 바꿔보면 다음과 같이 바꿀 수 있다.

 

@FunctionalInterface
interface EldestEntryRemovalFunction<K,V>{
	boolean remove(Map<K,V> map, Map.Entry<K,V> eldest); 
}


일단, removeEldestEntry 메서드는 인스턴스 메서드이므로 자기 자신도 함수 객체로 건네주어야 한다. Map<K, V> map

그리고 해당 인터페이스가 함수형 인터페이스임을 알 수 있도록 @FunctionalInterface 어노테이션을 붙여준다.

FunctionalInterface 어노테이션을 붙여주면 아래와 같은 이점이 있다.

 

  1. 해당 클래스의 코드나 문서를 읽을 이에게 인터페이스가 람다용으로 설계된 것임을 알려준다.
  2. 해당 인터페이스가 추상 메서드를 오직 하나만 가지고 있어야 컴파일 되게 해준다.
  3. 유지보수 과정에서 누군가 실수로 메서드를 추가하지 못하게 막아준다.

 

하지만 위와 같이 두 개의 인자를 받고 boolean 값을 리턴하는 모양의 인터페이스가 이미 자바 표준 라이브러리에 존재한다. BiPredicate 인터페이스다.

 

// java.util.function 패키지에 선언되어 있는 BiPredicate 형태
@FunctionalInterface
public interface BiPredicate<T, U> {
    boolean test(T t, U u);
}
// 별도로 인터페이스를 만들 필요 없다. BiPredicate 사용
BiPredicate<Map<String, String>, Map.Entry<String, String>> removalFunction
     = (map, eldest) -> map.size() > 5;

 

별도로 인터페이스를 선언할 필요없이 바로 사용하면 된다. "두 개의 인자를 받아 검증한다" 이외에 다른 기능이 존재하지 않는다면 별도로 만들지 않고 표준 라이브러리 기능을 사용하는 것을 권장한다.

 

아래 코드는 위에서 언급한 3가지 경우의 수를 테스트해볼 수 있는 코드이다.

OverrideLinkedHashMap 이 직접 클래스에 전략을 override 를 한 케이스(템플릿 메서드 패턴)이고, FunctionalLinkedHashMap 과 BiPredicateLinkedHashMap 이 전략을 함수 인자 형태로 전달한 케이스(템플릿 전략 패턴) 이다.

 

import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.BiPredicate;

public class MyLinkedHashMap{
    @FunctionalInterface
    public interface EldestEntryRemovalFunction<K, V> {
        boolean remove(Map<K, V> map, Map.Entry<K, V> eldest);
    }

    public static void main(String[] args) {
        // OverrideLinkedHashMap
        Map<Integer, Integer> overrideMap = new OverrideLinkedHashMap<>();
        for (int i = 0; i < 10; i++){
            overrideMap.put(i, i);
        }
        System.out.println(overrideMap);

        // FunctionalLinkedHashMap
        Map<Integer, Integer> functionalMap = new FunctionalLinkedHashMap<>((map, eldest) -> map.size() > 5);
        for (int i = 0; i < 10; i++){
            functionalMap.put(i, i);
        }
        System.out.println(functionalMap);

        // BiPredicateLinkedHashMap
        Map<Integer, Integer> functional2Map = new BiPredicateLinkedHashMap<>((map, eldest) -> map.size() > 5);
        for (int i = 0; i < 10; i++){
            functional2Map.put(i, i);
        }
        System.out.println(functional2Map);
    }
    private static class OverrideLinkedHashMap<K, V> extends LinkedHashMap<K, V> {
        @Override
        protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
            return size() > 5;
        }
    }
    private static class FunctionalLinkedHashMap<K, V> extends LinkedHashMap<K, V> {
        private final EldestEntryRemovalFunction<K, V> eldestEntryRemovalFunction;
        public FunctionalLinkedHashMap(EldestEntryRemovalFunction<K, V> function) {
            this.eldestEntryRemovalFunction = function;
        }
        @Override
        protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
            return eldestEntryRemovalFunction.remove(this, eldest);
        }
    }

    private static class BiPredicateLinkedHashMap<K, V> extends LinkedHashMap<K, V> {
        private final BiPredicate<Map<K, V>, Map.Entry<K, V>> eldestEntryRemovalFunction;
        public BiPredicateLinkedHashMap(BiPredicate<Map<K, V>, Map.Entry<K, V>> function) {
            this.eldestEntryRemovalFunction = function;
        }
        @Override
        protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
             return eldestEntryRemovalFunction.test(this, eldest);
        }
    }
}

 

 

java.util.function 패키지에 43개의 인스턴스가 포함되어 있으며 아래 6개의 인터페이스가 기본적인 인터페이스다. 나머지는 충분히 유추해낼 수 있다.

 

인터페이스 함수 시그니처
UnaryOperator<T> T apply(T t) String::toLowerCase
BinaryOperator<T> T apply(T t1, T t2) BigInteger::add
Predicate<T> boolean test(T t) Collection::isEmpty
Function<T,R> R apply(T t) Arrays::asList
Supplier<T> T get() Instant::now
Consumer<T> void accept(T t) System.out::println

 

 

아래의 6개 인터페이스들은 모두 참조 타용이다. 참고로 기본 인터페이스는, 기본 타입인 int, long, double 용으로 각 3개씩 변형이 생겨난다. 나머지는 코드나 문서를 참고하자.

 

보통의 경우에는 직접 작성하지 않고 표준 함수형 인터페이스를 사용해야 하지만, 구조적으로 같아도 직접 작성해야 하는 경우가 존재한다. 예를 들어 Comparator<T> 인터페이스는 구조적으로 ToIntBiFunction<T,U> 와 동일하다. 하지만 Comparator 가 독자적인 인터페이스로 남아야 하는 이유는 다음과 같다.

 

@FunctionalInterface
public interface Comparator<T> {
    int compare(T o1, T o2);
}

 

  1. API에서 자주 사용되며, 이름 자체가 용도를 명확히 설명해준다.
  2. 구현하는 쪽에서 반드시 따라야 하는 규약이 있다.
  3. 유용한 디폴트 메서드를 여러 개 제공할 필요가 있다. (위 Comparator 예에서는 비교자들을 조합하고 변환하는 메서드를 제공)

마지막으로, 위와 같은 함수형 인터페이스도 주의사항이 있다. 같은 위치의 인수로 받는 메서드들을 다중 정의해서는 안된다. 클라이언트에게 모호함을 안겨줄 뿐만 아니라, 둘 중에 어떤 타입인지 알기 위해 사용하는 쪽에서 타입 형변환을 해야할 수도 있다. "다중정의는 주의해서 사용하라" 라는 아이템 52 조언을 한 번 더 강조한다.

 

public interface ExecutorService extends Executor {
    <T> Future<T> submit(Callable<T> task);
    Future<?> submit(Runnable task);
}

 

Callable 과 Runnable 을 객체로 받는 두 가지 오버라이딩 형태로 함수 이름으로 한 번에 파악이 불가능하다.

 

"입력값과 반환값에 함수형 인터페이스 타입을 활용하라"

반응형
반응형

ITEM 43 "람다보다는 메서드 참조를 사용하라"

 

람다보다도 더 간결하게 작성할 수 있는 방법이 있다. 바로 메서드 참조이다.

 

map.merge(key, 1, (count, incr) -> count + incr);

 

위 코드는 자바 8 때, Map 에 추가된 merge 메서드이다. 키, 값, 함수를 인수로 받으며 주어진 키가 맵에 없다면 주어진 [키, 값] 쌍을 그대로 저장하고, 반대로 키가 있으면 [키, 함수의 결과] 쌍을 저장한다. 깔끔해 보이지만, count 와 incr 가 크게 하는 일 없이 공간만 차지한다. 자바 8이 되면서 Integer 클래스는 이 람다와 같은 기능을 가진 정적 메서드 sum 을 제공했다.

 

map.merge(key, 1, Integer::sum);

 

더 간결해진 것을 볼 수 있다. 하지만 메서드 참조는 함수의 이름만 명시하기 때문에 단번에 이해가 되지 않을 수도 있다.

매개변수의 이름 자체가 프로그래머에게 힌트를 준다면 람다가 더 좋은 선택지가 될 수도 있다. (매개변수가 여러 개이고 함수 이름으로 단 번에 파악이 안 된다면)

 

단 번에 파악할 목적이 아니고 해당 함수의 선언 부분으로 이동하는 수고로움을 감수할 수 있다면, 똑같은 인자 구성으로 함수를 만든 다음, 메서드 참조를 사용하는 것이 더 좋다. 메서드 참조에는 기능을 잘 드러내는 이름을 지어줄 수 있고, 친절한 설명을 문서에도 남길 수 있으니 말이다. 하지만 같은 클래스 안에 있는 기능을 호출하는 것이라면 람다가 더 간결하다.

 

// 1 번째 방법
service.execute(GoshThisClassNameIsHumongous::action);

// 2 번째 방법
service.execute(() -> action());

 

메서드 참조에는 아래와 같이 5가지 유형이 있다.

메서드 참조 유형 같은 기능을 하는 람다
정적 Integer::parseInt str -> Integer.parseInt(str)
한정적
(인스턴스)
Instant.now()::isAfter Instant then = Instant.now();
t -> then.isAfter(t)
비한정적
(인스턴스)
String::toLowerCase str -> str.toLowerCase()
클래스 생성자 TreeMap<K, V>::new () -> new TreeMap<K, V>()
배열 생성자 int[]::new len -> new Int[len]

 

"메서드 참조 쪽이 짧고 명확하다면 메서드 참조를 사용하고, 그렇지 않을 때만 람다를 사용한다"

반응형
반응형

ITEM 42 "익명 클래스보다는 람다를 사용하라"

 

Java 8 이전에는 자바에서 함수 타입을 표현할 때 추상 메서드를 하나만 담은 인터페이스를 사용했었다. 이런 인터페이스의 인스턴스를 함수 객체라고 하는데 다른 곳에서 절대 사용하지 않는 가벼운 함수 객체라면 함수 객체를 인자로 받는 공간에 바로 new 연산자를 통해 생성할 수 있었다.

 

Collections.sort(words, new Comparator<String>() {
	public int compare(String s1, String s2) {
		return Integer.compare(s1.length(), s2.length());
    }
});

 

이렇게 인터페이스 (위 예제에서는 Comparator 인터페이스) 를 바로 new 연산자를 통해 사용하고, 구체적인 전략은 익명 클래스 내부에 작성함으로써 전략 패턴의 장점도 사용할 수 있다. 하지만 매우 간단한 코드임에도 불구하고 매우 길다.

 

Java 8 부터는 추상 메서드 하나짜리 인터페이스는 람다식으로 간결하게 작성할 수 있다.

 

Collections.sort(words, (s1, s2) -> Integer.compare(s1.length(), s2.length()));

 

 

매개변수와 반환값에 대한 타입은 각각 String 과 Int 이지만 생략할 수 있다. 컴파일 타임 때 컴파일러가 맥락을 파악하고 자동으로 타입을 추론해주기 때문에 가능하다. 간혹 컴파일러가 타입을 추론하지 못해 컴파일 오류가 발생할 수도 있지만, 그럴 때 직접 프로그래머가 명시하면 된다. 타입을 명시해야 코드가 명확한 경우를 제외하고 람다의 모든 매개변수 타입은 생략하자.

 

더보기

제네릭에서 타입에 대한 정보를 얻을 수 있기 때문에 람다를 사용하는 함수들에서 제네릭을 적극적으로 사용하는 것을 권장한다. 로타입으로 구성했다면 컴파일 오류가 나서 람다 사용하는 부분에서 타입 변환을 해야한다.

 

메서드 참조를 사용하면 더 간결하게 할 수도 있다.

 

Collections.sort(words, comparingInt(String::length));

 

더 나아가 List 인터페이스에 추가된 sort 메서드를 이용하면 더 간결해진다.

 

words.sort(comparingInt(String::length));

 

아래와 같이, 열거 타입에서 상수 별 추상 메서드를 직접 구현하기 보다는 인스턴스 필드를 인자로 전달받아 간결하게 구현할 수도 있다. (람다로 구현하니 더 깔끔해진다)

 

public enum Operation {
	// 추상 함수를 내부에 구현하지 않고 인자로 전달 받음.
	PLUS("+", (x, y) -> x + y),
    MINUS("-", (x, y) -> x - y),
    TIMES("*", (x, y) -> x * y),
    DIVIDE("/", (x, y) -> x / y);
    
    private final String symbol;
    private final DoubleBinaryOperator op;
    
    Operation(String symbol, DoubleBinaryOperator op) {
    	this.symbol = symbol;
        this.op = op;
    }
    
    @Override
    public String toString() { return symbol; }
    
    public abstract double apply(double x, double y) {
    	return op.applyAsDouble(x, y);
   	};
}

 

위 코드에서 사용한 DoubleBinaryOperator 는 java.util.function 패키지가 제공하는 인터페이스 중 하나로 double 타입 인수 2개를 받아 double 타입 결과를 돌려주는 인터페이스이다.

 

간결하게 작성할 수 있다는 가장 큰 장점을 가지고 있는 람다도 사용 시 유의할 사항이 존재한다.

첫 번째, 람다는 이름이 없기 때문에 문서화하기가 곤란하다. 코드 자체로 동작이 명확히 설명되지 않거나 코드 줄 수가 많아진다면 람다를 쓰지 말아야 한다.

두 번째, 인스턴스로 만들어서 재사용할 때 람다를 쓸 수 없다. 익명 클래스를 사용해야 한다. 람다는 태생적으로 한 개의 추상 메서드를 가진 추상 클래스이므로 추상 메서드가 여러 개인 인터페이스의 인스턴스로도 활용이 불가능하다.

세 번째, 익명 클래스에서 this 지시자를 통해 자기 자신의 인스턴스를 참조하는 것과 다르게 람다에서 this 지시자는 람다 바깥 인스턴스를 참조한다.

네 번째, 람다도 익명 클래스처럼 역직렬화/직렬화 형태가 가상머신 VM 마다 구현이 다를 수 있어 유의해야 한다. private 정적 중첩 클래스의 인스턴스를 사용하는 것을 권장한다.

 

"꼭 익명 클래스를 사용해야 하는 자리가 아니라면 익명 클래스보다 간결하게 작성할 수 있는 람다를 사용해보자"

반응형
반응형

1. 문제요약

1715번: 카드 정렬하기 (acmicpc.net)

 

1715번: 카드 정렬하기

정렬된 두 묶음의 숫자 카드가 있다고 하자. 각 묶음의 카드의 수를 A, B라 하면 보통 두 묶음을 합쳐서 하나로 만드는 데에는 A+B 번의 비교를 해야 한다. 이를테면, 20장의 숫자 카드 묶음과 30장

www.acmicpc.net

각 묶음의 카드의 개수가 A, B 라고 주어질 때, 한 번 두 묶음을 합칠 때 A+B 번의 비교를 해야한다. 만약 A, B, C, D 의 카드 개수를 순서대로 하나로 합치려고 할 때, (A+B) + ((A+B)+C) + ((A+B+C)+D) 번 총 비교해야 한다.

카드의 개수가 연속으로 주어질 때, 최소한 몇 번 비교해야 하는지 알아내시오.

 

2. 문제예제

 

3. 문제풀이(수동)

주어진 문제예제를 살펴보면, (10+20) 번 비교하고, 다시 그 묶음을 40 번과 비교해서 (10+20) + (30+40) 번 비교해야 한다. 정렬이 되어 있지 않는 경우도 살펴볼 수 있다. 10, 20, 28, 25 라면, (10+20) 번 비교하고, (28+25) 번 비교한 다음 두 카드비교를 더해 (10+20) + (28+25) + (10+20) + (28+25) 로 최소 166 번 비교할 수도 있다. 

 

4. 팩트추출

Fact 1 : 한 번 합쳐진 값도 다시 하나의 값으로 인식해서 나머지 다른 값들과 비교해 최소끼리 계속 더해야 한다.

문제풀이(수동) 두 번째 예제를 보면 10+20 번 비교해서 한 번 합쳐진 값이 다른 값들과 비교하는데 나머지 25, 28 숫자보다 커서 25와 28끼리 합쳐야 한 것을 볼 수 있다.

 

Fact 2 :  Fact 1 번을 통해 연산한 결과 값들도 나머지 다른 값들과 함께 정렬되어 다시 연산에 쓰여야 된다는 것을 알 수 있다. 다시 말해보면, 삽입하면서 정렬이 되고 그 중 항상 최소값을 가져와야 한다.

 

5. 문제전략

Fact 2 번을 통해 연산한 결과 값이 삽입하면서 정렬이 되어야 하고, 항상 최소값을 가져와야 한다는 점 때문에 우선순위 큐를 사용하면 된다.

 

5. 소스코드

import java.io.*;
import java.util.PriorityQueue;

public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
        int N = Integer.parseInt(br.readLine());
        long total = 0;
        PriorityQueue<Long> pq = new PriorityQueue<>();
        for (int i = 0; i < N; i++) {
            long cardValue = Long.parseLong(br.readLine());
            pq.add(cardValue);
        }
        while (pq.size() > 1) {
            long first = pq.poll();
            long second = pq.poll();
            total += first + second;
            pq.add(first + second);
        }
        System.out.println(total);
    }
}

 

반응형

'알고리즘 > BOJ' 카테고리의 다른 글

[BOJ] 13334 철로 (Java)  (1) 2023.11.12
[BOJ] 24042 횡단보도 (Java)  (0) 2022.10.12
[BOJ] 10830 행렬 제곱 (Java)  (0) 2022.10.03
[BOJ] 9376 탈옥 (Java)  (0) 2022.10.02
[BOJ] 이항 계수 (Java)  (0) 2022.09.29

+ Recent posts