Call By Something

 

배경

  • 서비스에서 약 백오십만의 유저에게 알림을 전달하는 새로운 시스템에 대해 이야기를 하다, 어떤 방식으로 이걸 풀어야 하는지 생각하게 됨
  • API로 받아줄 경우 대상을 매번 추리는데 리소스가 크게 들거라 생각
  • 동시성 모델로 해당 대상 목록을 주기적으로 관리하며 알림을 전달하는건 어떨까 고민하게됨

 

잘못된 문제 인식 및 커뮤니케이션 오류

  • 동시성 모델로 선형적 자료형을 관리하고, 호출시마다 해당 자료형을 청크로 찍어내어 알림을 전달한다.
    • 예상된 문제는 값복사가 일어나서 메모리 사용량이 늘어날 거라 생각됨.
    • 대상을 추리기 위해서는 값복사 후 청크를 해야하나 vs 값복사 없이 청크를 말까
  • 안해도 된다고 해도 자바는 함수호출시 매개변수 전달로 인해 값복사가 일어나는데, 이건 문제가 아닐까?

 

체크

  • 오늘 생각하다보니 이게 잘못된거 같다는 생각이 들었고, 자바는 call by value인걸 인지는 하고 있지만, 그게 값복사가 항상 발생하는건 아닌듯 하여 확인하고자 함

 

과정

 

우선 아래 코드를 통해 150만개의 대상을 만들고 이걸 리스트에 저장함

val callBySomething = CallBySomething()
val list = callBySomething.list
println("hash=" + System.identityHashCode(list))
for(i in 1500000..3000000){
    list.add(BigInteger.valueOf(i.toLong()))
}

 

그리고 메모리 사용량을 체크

fun getMemoryUsage(): Long {
    val runtime = Runtime.getRuntime()
    return runtime.totalMemory() - runtime.freeMemory()
}
# 결과
hash=258952499
Memory used by myList: 120 MB

 

그리고 해당 객체를 매개변수로 사용하는 함수를 콜하여 해시값과 다시 한번 메모리 사용량을 체크

callBySomething(list)
val memoryAfter2 = getMemoryUsage()
println("Memory used by myList: ${(memoryAfter2 - memoryBefore)/1024/1024} MB")

fun callBySomething(list: MutableList<BigInteger>): MutableList<BigInteger> {
    println("hash=" + System.identityHashCode(list))
    return list
}
# 결과
hash=258952499
Memory used by myList: 120 MB
# 동일한 해시값과 변화없는 메모리 사용량

이번에는 깊은 복사를 해보자

val newList = copy(list)
val memoryAfter3 = getMemoryUsage()
println("Memory used by myList: ${(memoryAfter3 - memoryBefore)/1024/1024} MB")

fun copy(list: MutableList<BigInteger>): List<BigInteger> {
    return list.map { BigInteger(it.toByteArray())  }.also {
        println("hash=" + System.identityHashCode(it))
    }
}
# 결과
hash=1149319664
Memory used by myList: 236 MB
# 당연히 변경된 해시값과 그리고 증가한 메모리양

만약 copy의 코드가 아래와 같다면 list는 새로운 객체를 가리키겠지만, 그 안의 객체는 동일한 객체를 사용하여, 메모리 사용량에 차이가 거의 없음

// 왜냐하면 BigInteger도 객체이기 때문
// 새로운 list를 생성시 기존 BigInteger의 참조를 기반으로 생성
fun copy(list: MutableList<BigInteger>): List<BigInteger> {
    return list.map { it }.also {
        println("hash=" + System.identityHashCode(it))
    }
}

자, 그럼 이제 더 헷갈리는 건, 자바는 분명 call by value인데, 왜 마치 call by reference처럼 보이는걸까?

  • 분명한건 원시형 자료를 전달할때는 값복사를 한다는 점
  • 하지만 그게 아닐 경우에는 값의 참조를 복사하여 전달한다는 점
  • 하지만 그렇다고해서 포인터처럼 값의 참조 자체를 전달하는건 아님
  • 이게 어떤 차이가 있느냐, 아래에서 확인해보자
    fun referenceOfValue(list: MutableList<BigInteger>) {
        var list2 = list
        println("list2 hash=" + System.identityHashCode(list2))
        list2 = mutableListOf()
        println("list2 hash=" + System.identityHashCode(list2))
        println("list=${list.size}")
    }
    # 결과
    list2 hash=258952499
    list2 hash=2093631819
    list=1500001
  • 일단 코틀린은 매개변수가 불변이라 직접 수정은 못 하기에, 따로 변수에 할당하여 동일한 참조를 같는 변수를 만들어서 수정을 함
  • 만약 참조 자체를 전달하는거라면 해당 참조변수를 재선언 했을대, 참조주소 자체에 값을 할당을 하기에 변화가 일어난다
  • 하지만 자바는 재선언시 참조에 새로운 값을 할당하는게 아니라, 객체를 새로 생성하고 해당 객체의 참조를 재선언하는 필드에 할당하다보니 포인터를 사용하는것과는 차이가 있다
  • 이걸 한번 간단하게 포인터가 사용가능한 golang으로 확인해보자
func main() {
    str := "Hello, Go!"
    printString(&str)
    fmt.Println(str)
}

func printString(org *string) {
    fmt.Println(*org)
    cop := org
    fmt.Println(*cop)
    *cop = "Hello, Go2!"
    fmt.Println(*cop)
}
# 결과
Hello, Go!
Hello, Go!
Hello, Go2!
Hello, Go2!
  • 위에서 확인 할 수 있듯이, 원본을 값을 주소 연산자를 통해 받고 그걸 복사 후 포인터로 참조하여 값을 변경하면 원본도 변경된다

결론

  • 자바는 call by value이지만, 값의 참조를 복사하여 전달한다
  • 그렇기에 선언된 값을 매개변수로 사용한다고해도 값의 참조를 복사하여 전달하기에 메모리 사용량이 증가하지 않는다
  • 더불어 동시성모델로 관리하여 청크를 통해 실행한다면 BigInteger 또한 객체이기에 청크시 동일한 BigIneger의 참조값을 활용하기에 메모리 이슈도 없을거라 예상된다

OCP와 DIP를 지키기 위해
인터페이스와 구현체를 분리하고 다형성을 갖게끔 하는게 좋은 코드란건 알겠고,
결국 인터페이스를 상속받고 필요한 구현체만 구현하면 된다는걸 다들 알겠다고 한다.
 
근데 정작 사용을 어떻게 하는지 대부분 모르는 것 같다.
 
 
일단 개인적으로 그다지 좋지 않은 코드의 예시다.

    public interface PayService {

        void pay();
    }

    private class PayKakaoService implements PayService {

        @Override
        public void pay() {

        }
    }

    private class PaySamsungService implements PayService {

        @Override
        public void pay() {

        }
    }

    private class PayNaverService implements PayService {

        @Override
        public void pay() {

        }
    }

    private PayService payKakaoService;
    private PayService paySamsungService;
    private PayService payNaverService;

    @Getter
    @RequiredArgsConstructor
    public enum PayType {
        Kakao, Samsung, Naver;
    }

    @PostMapping("/payment")
    public ResponseEntity<?> pay(PayType payType) {

        if (payType == PayType.Kakao) {
            payKakaoService.pay();
        }

        if (payType == PayType.Samsung) {
            paySamsungService.pay();
        }

        if (payType == PayType.Naver) {
            payNaverService.pay();
        }

        return ResponseEntity.ok().build();
    }

    // 실행
    void callPayTest() {
        this.pay(PayType.Samsung);
    }

위 코드의 단점이 뭘까? 분명 인터페이스도 썻고, 필요에 따라 구현체도 만들었고,
근데 저기에 앱쁠페이를 추가한다면? 구현체를 만들지만

public ResponseEntity<?> pay(PayType payType)

요기 안에 있는 if문을 추가해야 하지 않겠는가?!
 
이거는 DIP에 위배 되는게 아닌가? 싶기도 하고, 

PayService ps = null;

if (payType == PayType.Kakao) {
    ps = payKakaoService;
}

if (payType == PayType.Samsung) {
    ps = paySamsungService;
}

if (payType == PayType.Naver) {
    ps = payNaverService;
}

ps.pay();

아님 뭐 메서드 내 필드를 이용해서 이렇게 바꿀까? 이것도 뭐 크게 다른건 없다고 느끼는데,
그렇다고 싱글톤 기반의 빈에서 클래스 필드를 맘대로 교체할 수도 없는 노릇이고,
 
좀 더 고차원 레이어에 영향이 안가는 방법의 코드를 작성 할 수는 없을까?
 
있다, Enum과 Provider를 사용하면 된다.
Spring Container는 등록된 모든 Bean에 대한 제어가 가능하다.
일단 등록되어 있으니 가져오는 것도 가능하다.
구조적으로는 위에 if문을 사용하는 것과 차이가 없지만,
 
적어도 인터페이스를 호출 하는 부분을 타입이나, 유형 때문에 바꿀 일은 없을 거다.
 
예시 코드 들어간다.
 
참고로 제 코드가 좋은 코드는 아닙니다용..제발...
 

    public interface PayService {

        void pay();
    }

    private class PayKakaoService implements PayService {

        @Override
        public void pay() {

        }
    }

    private class PaySamsungService implements PayService {

        @Override
        public void pay() {

        }
    }

    private class PayNaverService implements PayService {

        @Override
        public void pay() {

        }
    }

//    private PayService payKakaoService;
//    private PayService paySamsungService;
//    private PayService payNaverService;

    @Getter
    @RequiredArgsConstructor
    public enum PayType {
        Kakao(PayKakaoService.class), Samsung(PaySamsungService.class), Naver(
            PayNaverService.class);

        private final Class<? extends PayService> clazz;
    }

    public class PayServiceProvider {

        private static ApplicationContext applicationContext;
        private static Map<PayType, Class<? extends PayService>> payServiceMap = new HashMap<>();

        static {
            for (PayType payType : PayType.values()) {
                payServiceMap.put(payType, payType.getClazz());
            }
        }

        public static PayService service(PayType payType) throws Exception {
            Class<? extends PayService> payServiceClass = payServiceMap.get(payType);

            if (payServiceClass == null) {
                throw new Exception();
            }

            return applicationContext.getBean(payServiceClass);
        }
    }

    @PostMapping("/payment")
    public ResponseEntity<?> pay(PayType payType) throws Exception {

//        PayService ps = null;
//
//        if (payType == PayType.Kakao) {
//            ps = payKakaoService;
//        }
//
//        if (payType == PayType.Samsung) {
//            ps = paySamsungService;
//        }
//
//        if (payType == PayType.Naver) {
//            ps = payNaverService;
//        }
//
//        ps.pay();

        PayServiceProvider.service(payType).pay();

        return ResponseEntity.ok().build();
    }

    // 실행
    void callPayTest() throws Exception {
        this.pay(PayType.Samsung);
    }

일단, 해당 코드는 provider에서 사용하는 applicationContext에 대한 주입이 없긴하다.
요건 이제 @PostConstruct를 사용해서 주입하면 되긴합니다요.
물론 주입하는 함수도 provider에 static으로 만들어 줘야 합니다.
(대충 읽고 코드만 복붙하면 안되게 하려는 함정...)
 
대강 저런식이면, 앞으로 앱쁠페이든, 티머니든 추가되면 Enum과 구현체의 추가만으로 솔리드 뭐시기를 위배하지 않으면서 해결가능하지 않을까?
 
사실 Provider 구현하기 귀찮다.
유지 보수 측면에서는 좋긴하나, 처음 구현할때 걍 if else 쓰는게 백배 편하다.
시간 비용을 초기에 쓸거냐 나중에 쓸거냐 차이긴한데,
 
아래는 좀 더 편한 방법

private List<PayService> payServices;

@PostMapping("/payment")
public ResponseEntity<?> pay(PayType payType) throws Exception {

    payServices.stream().filter(payService -> payService.check(payType)).findFirst()
        .ifPresent(PayService::pay);
    return ResponseEntity.ok().build();
}

 
스프링은 주입시 배열로 필드를 선언하면 해당 인터페이스에 해당하는 빈들을 모두 가져 오는데요, 이걸 몰랐던건 아닌데, 이렇게 응용할 생각은 못 했네요. 멍청이...
 
아는분이 알려주신건데 아래 링크 통해 배우셨대요!
https://youtu.be/3MTf43_RcVM

 

디스크 컨트롤러

하드디스크는 한 번에 하나의 작업만 수행할 수 있습니다. 디스크 컨트롤러를 구현하는 방법은 여러 가지가 있습니다. 가장 일반적인 방법은 요청이 들어온 순서대로 처리하는 것입니다.

예를들어

- 0ms 시점에 3ms가 소요되는 A작업 요청 - 1ms 시점에 9ms가 소요되는 B작업 요청 - 2ms 시점에 6ms가 소요되는 C작업 요청

와 같은 요청이 들어왔습니다. 이를 그림으로 표현하면 아래와 같습니다.

한 번에 하나의 요청만을 수행할 수 있기 때문에 각각의 작업을 요청받은 순서대로 처리하면 다음과 같이 처리 됩니다.

- A: 3ms 시점에 작업 완료 (요청에서 종료까지 : 3ms) - B: 1ms부터 대기하다가, 3ms 시점에 작업을 시작해서 12ms 시점에 작업 완료(요청에서 종료까지 : 11ms) - C: 2ms부터 대기하다가, 12ms 시점에 작업을 시작해서 18ms 시점에 작업 완료(요청에서 종료까지 : 16ms)

이 때 각 작업의 요청부터 종료까지 걸린 시간의 평균은 10ms(= (3 + 11 + 16) / 3)가 됩니다.

하지만 A → C → B 순서대로 처리하면

- A: 3ms 시점에 작업 완료(요청에서 종료까지 : 3ms) - C: 2ms부터 대기하다가, 3ms 시점에 작업을 시작해서 9ms 시점에 작업 완료(요청에서 종료까지 : 7ms) - B: 1ms부터 대기하다가, 9ms 시점에 작업을 시작해서 18ms 시점에 작업 완료(요청에서 종료까지 : 17ms)

이렇게 A → C → B의 순서로 처리하면 각 작업의 요청부터 종료까지 걸린 시간의 평균은 9ms(= (3 + 7 + 17) / 3)가 됩니다.

각 작업에 대해 [작업이 요청되는 시점, 작업의 소요시간]을 담은 2차원 배열 jobs가 매개변수로 주어질 때, 작업의 요청부터 종료까지 걸린 시간의 평균을 가장 줄이는 방법(포인트3)으로 처리하면 평균이 얼마가 되는지 return 하도록 solution 함수를 작성해주세요. (단, 소수점 이하의 수는 버립니다) 포인트1

제한 사항

  • jobs의 길이는 1 이상 500 이하입니다. 
  • jobs의 각 행은 하나의 작업에 대한 [작업이 요청되는 시점, 작업의 소요시간] 입니다.
  • 각 작업에 대해 작업이 요청되는 시간은 0 이상 1,000 이하입니다.
  • 각 작업에 대해 작업의 소요시간은 1 이상 1,000 이하입니다.
  • 하드디스크가 작업을 수행하고 있지 않을 때에는 먼저 요청이 들어온 작업부터 처리합니다. 포인트2 

 

입출력 예

jobsreturn

[[0, 3], [1, 9], [2, 6]] 9

입출력 예 설명

문제에 주어진 예와 같습니다.

  • 0ms 시점에 3ms 걸리는 작업 요청이 들어옵니다.
  • 1ms 시점에 9ms 걸리는 작업 요청이 들어옵니다.
  • 2ms 시점에 6ms 걸리는 작업 요청이 들어옵니다.

프로그래머스 힙 디스크 컨트롤러 레벨3의 문제.

저는 1년차 같은 10년차 개발자라 그런지...

이 문제 시간제한 있는 상황이라면 저는 무조건 탈락할것 같네요.

문제가 길어서 이해하기도 쉽지 않을뿐더러(책을 많이 읽다보니, 글 읽을때 대각선으로 속독하는 버릇이 있어서...ㅠㅠ)

시간제한 때문에 긴장도 해서 뇌근육이 굳을지도?!

 

일단 위는 복붙, 그 중 포인트는 '소수점 이하의 수는 버립니다'

 

포인트1 : 이 부분이 눈에 안뛰어서 코드를 짜고서 어 뭐지 왜 안대지? 될 코드인데...한참 고민한...

포인트2 : 디스크가 작업을 안하면 먼저 요청이 온 애들을 넣는다라...2초에 작업이 끝나서 5초에 작업이 1개만 들어왔으면 모를까, 3개가 들어왔다면 걔중에서도 선별을 해줘야 하겠죠?

포인트3 : 동시간 요청에 1초 간격의 작업 3개가 들어왔다고 가정!

{{0, 3}, {0, 4}, {0, 5}}

작업시간 순 정렬시 3 + 7 + 12, 역순시 5 + 9 + 17

딱 봐도 동시간 요청은 작업시간이 낮은 순으로 정렬해주는게 좋을듯.

 

요 세가지를 생각하며 코딩했지만, 의외로 놓치는 부분이 생겨서...

점프 구간 부분을 생각을 못 해서...좀 헤맨 ㅠ

(저 아래 주석 처리한 부분)

 

 

코드에 주석을 잘 하는편은 아닌데, 

저도 좀 헷갈려서 혹시 모를 보시는분을 위해 배려 차원에서 주석을 했습니다.

계산을 위해 지정해야하는 변수가 좀 헷갈릴뿐, 전체적인 난이도는 그다지 높지 않은듯 합니다.

가장 중요한건 요구사항에 맞게 정렬만 하면 되는일인데, 막상 정렬 하고서도 위 부분땜에 고생했네요.

 

 

package com.test.heap;

import java.util.Arrays;
import java.util.PriorityQueue;

public class Heap2 {

	public static void main(String[] args) {
		
		int[][] arr = {{0,3}, {1,9}, {2,6}};
//		int[][] arr = {{0,3}, {1,3}, {1,3}, {1,3}, {15,3}};
		
		int r = solution(arr);
		
		System.out.println(r);
	}
	
    public static int solution(int[][] jobs) {
        
        PriorityQueue<Total> ts = new PriorityQueue<>();
        PriorityQueue<Wait> ws = new PriorityQueue<>();
        
        // 배열을 우선순위 큐로 변환
        Arrays.stream(jobs).forEach(job -> {
        	Total t = new Total(job);
        	ts.add(t);
        });

        ws.add(new Wait(ts.poll()));
        
        // 흐른시간
        int totTime = 0;
        // sum(각 프로세스별 처리시간 = 기다린시간 + 일한시간) 
        int totProcessTime = 0;
        while(!ws.isEmpty()) {
        	Wait w = ws.poll();
        	
        	// 기다린시간
        	int waitTime = 0;
        	// 점프한시간(흐른 시간을 위해)
        	int jumptime = 0;
        	
        	
        	if(w.getInterruptTIme() > totTime) { // 요청시간이 흐른 시간보다 미래인 경우
        		waitTime = 0; // 대기는 0보다 작을 수 없으니까
        		jumptime = w.getInterruptTIme() - totTime; // 흐른시간을 계산하기 위해 미래를 현재로 만들기 위한 점프 구간
        	}else { // 대기를 0초 이상 한 경우
        		waitTime = totTime - w.getInterruptTIme();
        	}
        	
        	// 처리시간 = 기다린시간 + 일한시간 
        	int processTime = waitTime + w.getWorkingTime();
        	
        	// 흐른시간 = 기존흐른시간 + 일한시간 + 점프한시간
        	totTime += w.getWorkingTime() + jumptime;
        	
        	totProcessTime += processTime;
        	
    		while((!ts.isEmpty() && ts.peek().getInterruptTIme() <= totTime) // 총대기열이 있고, 흐른시간안에(처리하는동안 or 과거에) 요청이 온 경우가 있을때 
    				|| !ts.isEmpty() && ws.isEmpty()) { // 처리대기열이 비었는데, 총대기열이 남은 경우 (미래작업이 있는 경우)
    			
    			ws.add(new Wait(ts.poll()));
    			
    		}
        	
        }
        
//        return (int)Math.round((double)totProcessTime/jobs.length);
        return totProcessTime/jobs.length;
        
//        int answer = 0;
//        
//        PriorityQueue<Total> ts = new PriorityQueue<>();
//        
//        PriorityQueue<Wait> ws = new PriorityQueue<>();
//        
//        Arrays.stream(jobs).forEach(job -> {
//        	Total t = new Total(job);
//        	ts.add(t);
//        });
//
//        ws.add(new Wait(ts.poll()));
//        
//        
//        int totTime = 0;
//        
//        int lastEndTime = 0;
//        while(!ts.isEmpty() || !ws.isEmpty()) {
//        	Wait w = ws.peek();
//        	
//        	int waitTime = lastEndTime - w.getInterruptTIme();
//        	waitTime = waitTime < 0 ? 0 : waitTime;
//        	int processTime = waitTime + w.getWorkingTime();
//        	
//        	if(lastEndTime > totTime) {
//        		lastEndTime += w.getWorkingTime();
//        		ws.remove();
//        		answer += processTime;
//        	}
//        	
//    		while(!ts.isEmpty() && ts.peek().getInterruptTIme() <= totTime) {
//    			ws.add(new Wait(ts.poll()));
//    		}
//    		totTime++;
//        	
//        }
//        
//        
//        return answer/jobs.length;    
    }
    
}

// 전체대기열
class Total implements Comparable<Total>{

	private int interruptTIme;
	
	private int workingTime;
	
	Total (int[] job){
		this.interruptTIme = job[0];
		this.workingTime = job[1];
	}
	

	public int getInterruptTIme() {
		return interruptTIme;
	}

	public int getWorkingTime() {
		return workingTime;
	}


	// 1. 들어온시간 2. 일하는시간 
	@Override
	public int compareTo(Total o) {
		
		if(this.getInterruptTIme() == o.getInterruptTIme()) {
			return this.getWorkingTime() - o.getWorkingTime();
		}else {
			return this.getInterruptTIme() - o.getInterruptTIme();
		}
		
	}
}

// 진행대기열
class Wait implements Comparable<Wait>{

	private int interruptTIme;
	
	private int workingTime;
	
	Wait (Total t){
		this.interruptTIme = t.getInterruptTIme();
		this.workingTime = t.getWorkingTime();
	}
	
	public int getInterruptTIme() {
		return interruptTIme;
	}

	public int getWorkingTime() {
		return workingTime;
	}

	// 1. 일하는 시간
	@Override
	public int compareTo(Wait o) {
		return this.getWorkingTime() - o.getWorkingTime();
	}
}

문제 설명

전화번호부에 적힌 전화번호 중, 한 번호가 다른 번호의 접두어인 경우가 있는지 확인하려 합니다.
전화번호가 다음과 같을 경우, 구조대 전화번호는 영석이의 전화번호의 접두사입니다.

  • 구조대 : 119
  • 박준영 : 97 674 223
  • 지영석 : 11 9552 4421

전화번호부에 적힌 전화번호를 담은 배열 phone_book 이 solution 함수의 매개변수로 주어질 때, 어떤 번호가 다른 번호의 접두어인 경우가 있으면 false를 그렇지 않으면 true를 return 하도록 solution 함수를 작성해주세요.

제한 사항

  • phone_book의 길이는 1 이상 1,000,000 이하입니다.
    • 각 전화번호의 길이는 1 이상 20 이하입니다.
    • 같은 전화번호가 중복해서 들어있지 않습니다.

입출력 예제

phone_bookreturn

["119", "97674223", "1195524421"] false
["123","456","789"] true
["12","123","1235","567","88"] false

입출력 예 설명

입출력 예 #1
앞에서 설명한 예와 같습니다.

입출력 예 #2
한 번호가 다른 번호의 접두사인 경우가 없으므로, 답은 true입니다.

입출력 예 #3
첫 번째 전화번호, “12”가 두 번째 전화번호 “123”의 접두사입니다. 따라서 답은 false입니다.


알림

2021년 3월 4일, 테스트 케이스가 변경되었습니다. 이로 인해 이전에 통과하던 코드가 더 이상 통과하지 않을 수 있습니다.

 

다른게 아니라 빨간 글씨가 포인트...ㅎㄷㄷ;;

이것저것 막 해봐도 시간초과 나길래...

구글링도 해봐도 시간초과 나길래...

해시관련된걸 써야하나 싶어서...

 

import java.util.HashMap;
import java.util.HashSet;

class Solution {
    public boolean solution(String[] phone_book) {
        
        HashMap<String, String> m1 = new HashMap<>();
        HashSet<String> t = new HashSet<>();
		
        for(int i=0; i<phone_book.length; i++) {
            t.add(phone_book[i]);
        }
		
        for ( String source : phone_book) {
            for( int i=1; i<= source.length(); i++) {
				
                if(!source.substring(0,i).equals(source)) {
                    m1.put(source.substring(0,i), source);
                }
            }
        }
		
        for(String s1 : t) {
            if(m1.containsKey(s1)) {
                return false;
            }
        } 
        
        return true;
    }
}

 

루프가 많아서 수행시간은 비록 오래걸리지만,

일단 해시를 사용하기만하고 작성하자 했는데, 이게 통과되네...ㅡ_ㅡ;

+ Recent posts