개요

오늘도 5천원에 헛된 희망을 구매하고 로또를 보지만 언제나 낙첨...

1등이 6개나 되는군요. 왜 나는 안될까란 의구심을 품으며 이전 회차의 1등 복권수를 훓어봤습니다.

13개... 20개... 15개... 많이도 되네요. 1/815만 이라는데 어떻게 이렇게 많을까요? 

1177회에 총판매금액은 115,411,832,000원 이라고 합니다. 1000원에 1장이니까 115,411,832장이 판매되었겠네요. 나눠보면 14.1장 얼추 맞는것 같네요.

 

예전 기억이 떠오릅니다. JAVA를 처음배울때 로또 생성하는걸 해봤었거든요. 오랜만에 한번 구현해보았습니다.

 

의식의 흐름대로 만들어보는 로또 시뮬레이션 최적화

 

 

 

요구사항

만들기전에 간단하게 요구사항을 정해봅시다.

1. 당첨번호 6개와 보너스번호 1개를 먼저 지정합니다.
2. 숫자의 범위는 1 ~ 45까지로 합니다.
3. 지정된 수만큼 로또 번호를 생성해 각 등수의 개수를 출력합니다.
4. 시작부터 종료까지의 시간을 측정합니다.

 

 

 

 

구현

 

MK-1

public class Main {

    static final Set<Integer> targetNumbers = Set.of(3,7,15,16,19,43);
    static final int bonusNumber = 21;
    static final Map<Integer, Integer> ranking = new HashMap<>();

    public static void main(String[] args) {
        long start = System.currentTimeMillis();

        int count = 115_411_832;

        for (int i = 0; i < count; i++) {
            int[] numbers = generateNumbers();
            int rank = evaluate(numbers);
            if (rank < 6) {
                ranking.put(rank, ranking.getOrDefault(rank, 0) + 1);
            }
        }

        long end = System.currentTimeMillis();
        System.out.println(end - start + "/ms 소요");
        
        print();

    }

    private static void print() {
        for (int i = 1; i < 6; i++) {
            System.out.printf("%d등 : %d명\n", i, ranking.get(i));
        }
    }

    private static int evaluate(int[] numbers) {
        int count = 0;
        boolean hasBonusNumber = false;
        for (int number : numbers) {
            if (targetNumbers.contains(number)) {
                count++;
            } else if (number == bonusNumber) {
                hasBonusNumber = true;
            }
        }

        if (count == 6) { // 다 맞으면 1등
            return 1;
        }
        if (hasBonusNumber) { // 보너스번호 맞으면 +1
            count++;
        }
        return switch (count) {
            case 6 -> 2; // 6개 맞으면 2등
            case 5 -> 3; // 5개 맞으면 3등
            case 4 -> 4; // 4개 맞으면 4등
            case 3 -> 5; // 3개 맞으면 5등
            default -> 6; // 그 외 6등
        };
    }

    private static int[] generateNumbers() {
        return new Random()
                .ints(1, 46)
                .limit(6)
                .distinct()
                .sorted()
                .toArray();
    }
}

 

생각나는대로 만들어봤습니다. 대충 보이시죠?

  1. count 만큼 반복
  2. generateNumbers()에서 로또번호 생성
  3. evaluate에서 등수 반환
  4. 1 ~ 5등까지 ranking에 넣고 출력

 

// 62697/ms 소요
// 1등 : 6명
// 2등 : 53명
// 3등 : 8592명
// 4등 : 279948명
// 5등 : 3602047명

근데 세상에.. 62.7초나 걸렸군요. 너무 성의 없이 만든것 같습니다. 필요 없는 부분을 덜어내야겠어요.

 

 

MK-2 추상화

public interface LotterySimulator {

    void execute();
    void print();
}

public class Times {

    public static void timer(Runnable runnable) {
        long start = System.currentTimeMillis();
        runnable.run();
        long end = System.currentTimeMillis();
        System.out.println(end - start + "/ms 소요");
    }
    
}

public abstract class AbstractFixedCountLotterySimulator implements LotterySimulator {

    protected final int count;

    protected AbstractFixedCountLotterySimulator(int count) {
        this.count = count;
    }
    
    abstract protected void innerExecute();

    @Override
    public final void execute() {
        Times.timer(this::innerExecute);
    }

}

먼저 지저분한 코드를 로또 시뮬레이션을 완전 추상화 했습니다.

 

public class LotterySimulatorMK2 extends AbstractFixedCountLotterySimulator {

    private final Set<Integer> targetNumbers;
    private final int bonusNumber;
    private final Map<Integer, Integer> ranking = new HashMap<>();

    public LotterySimulatorMK2(int count, int[] targetNumbers, int bonusNumber) {
        super(count);
        this.targetNumbers = Arrays.stream(targetNumbers).boxed().collect(Collectors.toSet());
        this.bonusNumber = bonusNumber;
    }

    @Override
    protected void innerExecute() {
        for (int i = 0; i < count; i++) {
            int[] numbers = generateNumbers();
            int rank = evaluate(numbers);
            if (rank < 6) {
                ranking.put(rank, ranking.getOrDefault(rank, 0) + 1);
            }
        }
    }

    private int[] generateNumbers() {
        return new Random()
                .ints(1, 46)
                .limit(6)
                .distinct()
                .sorted()
                .toArray();
    }

    private int evaluate(int[] numbers) {
        int count = 0;
        boolean hasBonusNumber = false;
        for (int number : numbers) {
            if (targetNumbers.contains(number)) {
                count++;
            } else if (number == bonusNumber) {
                hasBonusNumber = true;
            }
        }

        if (count == 6) { // 다 맞으면 1등
            return 1;
        }
        if (hasBonusNumber) { // 보너스번호 맞으면 +1
            count++;
        }
        return switch (count) {
            case 6 -> 2; // 6개 맞으면 2등
            case 5 -> 3; // 5개 맞으면 3등
            case 4 -> 4; // 4개 맞으면 4등
            case 3 -> 5; // 3개 맞으면 5등
            default -> 6; // 그 외 6등
        };
    }

    @Override
    public void print() {
        for (int i = 1; i < 6; i++) {
            System.out.printf("%d등 : %d명\n", i, ranking.get(i));
        }
    }

}

그리고 이전 코드를 똑같이 붙혀넣었습니다.

접근제한자를 보면 아시겠지만 LotterySimulator 인터페이스로는 내부 코드를 볼수없습니다. 내부 코드가 수정되어도 외부에서는 크게 중요하지 않다는거죠.

AbstractFixedCountLotterySimulator

클래스에서 execute를 Override할때 final을 붙혀줘서 더이상의 상속을 막아 시간을 재는 로직이 override할 수 없도록 막았습니다.

 

public class Main2 {

    public static void main(String[] args) {
        int count = 115_411_832;
        LotterySimulator simulator = new LotterySimulatorMK2(count);
        simulator.execute();
        simulator.print();
    }

}

이제 실행해보면

// 70337/ms 소요
// 1등 : 12명
// 2등 : 62명
// 3등 : 8488명
// 4등 : 281238명
// 5등 : 3600109명

추상화했다고 7초나 더 걸리네요...

 

62697ms -> 70337ms

 

MK-3 최적화(1)

로또 생성 파트부터 보겠습니다.

1. Random 객체 재사용
2. sorted() 제거

 

생각해보니까 Random 객체를 매 번 생성하고있었습니다. 재사용하도록 고쳐주고, 정렬하는부분도 없애겠습니다. 해시값으로 값을 확인하기때문에 정렬을 필요 없습니다.

 

    @Override
    protected void innerExecute() {
        Random random = new Random();
        for (int i = 0; i < count; i++) {
            int[] numbers = generateNumbers(random);
            int rank = evaluate(numbers);
            if (rank < 6) {
                ranking.put(rank, ranking.getOrDefault(rank, 0) + 1);
            }
        }
    }

    protected int[] generateNumbers(Random random) {
        return random
                .ints(1, 46)
                .limit(6)
                .distinct()
                .toArray();
    }
    
    // 44373/ms 소요

 

62697ms -> 70337ms -> 44373ms

 

 

MK-4 최적화(2)

로또 번호를 맞추는 메소드를 다시 한 번 보겠습니다.

배열의 재사용
ints(1, 46) -> ints(45) + 1

generateNumbers 메소드가 매번 새로운 객체를 반환하는게 마음에 안듭니다.

 

@Override
protected void innerExecute() {
    Random random = new Random();
    int[] numbers = new int[6];
    boolean[] used = new boolean[46];

    for (int i = 0; i < count; i++) {
        generateNumbers(random, numbers, used);
        int rank = evaluate(numbers);
        if (rank < 6) {
            ranking.put(rank, ranking.getOrDefault(rank, 0) + 1);
        }
    }
}

private void generateNumbers(Random random, int[] numbers, boolean[] used) {
    Arrays.fill(used, false);
    for (int i = 0; i < 6; i++) {
        int num;
        do {
            num = random.nextInt(45) + 1;
        } while (used[num]);

        used[num] = true;
        numbers[i] = num;
    }
}

int 배열 하나와 중복확인을 위한 boolean 배열 하나를 만들어서 재사용하도록 했습니다.

Arrays.fill(used, false)로 초기화 시켜줍니다.

 

근데 Arrays.fill(used, false) 도 마음에 안듭니다. 한번 초기화할때마다 46칸짜리 배열을 돌아야합니다. 사용된 곳만 초기화하고싶습니다.

    private void generateNumbers(Random random, int[] numbers, boolean[] used) {
//        Arrays.fill(used, false);
        for (int i = 0; i < 6; i++) {
            int num;
            do {
                num = random.nextInt(45) + 1;
            } while (used[num]);

            used[num] = true;
            numbers[i] = num;
        }
        for (int number : numbers) {
            used[number] = false;
        }
    }
// 16386/ms 소요

for 문 해결합니다.

 

 

62697ms -> 70337ms -> 44373ms -> 16386ms

 

 

MK-5 최적화(3)

로또 번호를 맞추는 메소드를 보겠습니다.

count 비교문 최적화
        if (count == 6) { // 다 맞으면 1등
            return 1;
        }
        if (hasBonusNumber) { // 보너스번호 맞으면 +1
            count++;
        }
        return switch (count) {
            case 6 -> 2; // 6개 맞으면 2등
            case 5 -> 3; // 5개 맞으면 3등
            case 4 -> 4; // 4개 맞으면 4등
            case 3 -> 5; // 5개 맞으면 5등
            default -> 6; // 그 외 6등
        };

6등이 대부분임에도 불구하고 1등부터 모두 비교하고 있습니다.

6개 -> 2등, 5개 -> 3등 ... 을보면 default를 제외하고 모두 8 - count = rank 가 성립됩니다. 이를 반영해줍시다

 

        if (count < 2) {
            return 6;
        } else if (count == 6) {
            return 1;
        } else if (hasBonusNumber) {
            count++;
        }
        return 8 - count;
        
 // 16116/ms 소요

 

0또는 1개를 맞은사람은 보너스번호를 맞췄다해도 6등입니다. : 6등 반환

6개 모두 맞춘사람은 보너스점수를 비교할 필요가 없습니다. : 1등 반환

보너스점수를 맞췄다면 count를 1 증가시킵니다.

 

그리고 8 - count를 해줍니다.

 

62697ms -> 70337ms -> 44373ms -> 16386ms -> 16116ms

전혀 차이가 없네요.

 

 

 

MK-6 최적화(4)

1. 당첨번호 Set -> boolean[] 변경
2. 등수 Map -> int[] 변경

 

수백번도 아니고 1억번 이상의 반복문에서 Set의 contains로 매번 확인하는 건 너무 비효율적이라는 생각이 들었습니다. boolean[] 으로 확인하도록 해봅니다.

 

    private final boolean[] targetNumbers = new boolean[46];
    private final int bonusNumber;
    private final Map<Integer, Integer> ranking = new HashMap<>();

    public LotterySimulatorMK6(int count, int[] targetNumbers, int bonusNumber) {
        super(count);
        for (int target : targetNumbers) {
            this.targetNumbers[target] = true;
        }
        this.bonusNumber = bonusNumber;
    }

Set에서 boolean[] 으로 변경하고 targetNumbers에 존재하는 위치만 true로 변경해줍니다.

 

        for (int number : numbers) {
            if (targetNumbers[number]) {
                count++;
            } else if (number == bonusNumber) {
                hasBonusNumber = true;
            }
        }

evaluate 메소드에서 targetNumbers[number] 로 변경해줍니다.

 

 

등수를 표현하는 ranking의 타입도 Map에서 int[] 로 변경해줍니다.

private final int[] ranking = new int[6];

    @Override
    protected void innerExecute() {
        Random random = new Random();
        int[] numbers = new int[6];
        boolean[] used = new boolean[46];

        for (int i = 0; i < count; i++) {
            generateNumbers(random, numbers, used);
            int rank = evaluate(numbers);
            if (rank < 6) {
//                ranking.put(rank, ranking.getOrDefault(rank, 0) + 1);
                ranking[rank]++;
            }
        }
    }
    
    @Override
    public void print() {
        for (int i = 1; i < 6; i++) {
            System.out.printf("%d등 : %d명\n", i, ranking[i]);
        }
    }
    
 // 9754/ms 소요

 

62697ms -> 70337ms -> 44373ms -> 16386ms -> 16116ms -> 9754ms

 

 

MK-7 최적화(5) - 비트마스크

당첨번호를 boolean[] -> long으로 바꿔 bit로 관리한다

 

로또번호는 45개가 최대니 46개의 비트만 있으면 된다. 비트연산으로 처리해보자

 

    private final long TARGET_MASK;
    private final int bonusNumber;
    private final int[] ranking = new int[6];

    public LotterySimulatorMK7(int count, int[] targetNumbers, int bonusNumber) {
        super(count);
        this.TARGET_MASK = toBitmask(targetNumbers);
        this.bonusNumber = bonusNumber;
    }

    private long toBitmask(int[] numbers) {
        long bitmask = 0;
        for (int number : numbers) {
            bitmask |= 1L << number;
        }
        return bitmask;
    }
    
    private int evaluate(int[] numbers) {
        int count = 0;
        boolean hasBonusNumber = false;
        for (int number : numbers) {
            if ((TARGET_MASK & (1L << number)) != 0) {
//            if (targetNumbers[number]) {
                count++;
            } else if (number == bonusNumber) {
                hasBonusNumber = true;
            }
        }

        if (count < 2) {
            return 6;
        } else if (count == 6) {
            return 1;
        } else if (hasBonusNumber) {
            count++;
        }
        return 8 - count;
    }
// 9154/ms 소요

 

예를들어서 로또 번호는 3개라고 가정하고 {1, 3, 5} 가 당첨번호라고 가정하면

1L << 1 과  1L << 3 과 1L << 5 라고 할 수 있다.

1L은 000...000001이기때문에 왼쪽으로 1칸 시프트는 000...000010이다.

3은 왼쪽으로 3칸 시프트이기 때문에 000...001000 이다

5는 왼쪽으로 5칸 시프트이기 때문에 000...100000 이다

{1, 3, 5} 를 OR 연산하면 000...101010 이다 이를 이용해 내 복권번호를 << 연산으로 계산해서 AND 연산해보면 값을 알 수 있다.

내 로또 번호중에 3을 검증한다고 했을때 101010 & 001000 != 0 이렇게 된다

 

62697ms -> 70337ms -> 44373ms -> 16386ms -> 16116ms -> 9754ms -> 9154ms

 

한 번 더 나아가서 generateNumbers 메소드에서 만들어진 로도번호를 비트연산해서 long 타입을 반환받고 TARGET_MASK와 한번에 AND 연산해서 1의 개수를 세면 되는거 아닐까? 한번 해보자

 

    private final long TARGET_MASK;
    private final long BONUS_TARGET_MASK;
    private final int[] ranking = new int[6];

    public LotterySimulatorMK7_BIT(int count, int[] targetNumbers, int bonusNumber) {
        super(count);
        this.TARGET_MASK = toBitmask(targetNumbers);
        this.BONUS_TARGET_MASK = 1L << bonusNumber;
    }
    
    @Override
    protected void innerExecute() {
        Random random = new Random();

        for (int i = 0; i < count; i++) {
            long bitmask = generateNumbers(random);
            int rank = evaluate(bitmask);
            if (rank < 6) {
                ranking[rank]++;
            }
        }
    }

    private long generateNumbers(Random random) {
        long bitmask = 0L;
        int count = 0;

        while (count < 6) {
            long bit = 1L << random.nextInt(45) + 1;

            if ((bitmask & bit) == 0) {
                bitmask |= bit;
                count++;
            }
        }
        return bitmask;
    }

    private int evaluate(long bitmask) {
        int matchCount = Long.bitCount(TARGET_MASK & bitmask);

        if (matchCount < 2) return 6;
        if (matchCount == 6) return 1;

        boolean hasBonusNumber = (bitmask & BONUS_TARGET_MASK) != 0;

        return hasBonusNumber ? 8 - (matchCount - 1) : matchCount;
    }
// 7484/ms 소요

당첨번호와 보너스번호 모두 비트연산으로 바꾸었다 evaluate에서 AND 연산으로 일치하는 자리를 카운트해서 연산하도록 변경했다.

 

62697ms -> 70337ms -> 44373ms -> 16386ms -> 16116ms -> 9754ms -> 9154ms -> 7484ms

 

 

MK-8 최적화(6) - ThreadLocalRandom

Random 객체를 ThreadLocalRandom 객체로 변환

 

    @Override
    protected void innerExecute() {
        Random random = ThreadLocalRandom.current();

        for (int i = 0; i < count; i++) {
            long bitmask = generateNumbers(random);
            int rank = evaluate(bitmask);
            if (rank < 6) {
                ranking[rank]++;
            }
        }
    }
// 3586/ms 소요

 

new Random() -> ThreadLocalRandom.current()로 변경한다.

ThreadLocalRandom은 Ramdom의 자식객체이기때문에 구현체만 변경해주면 된다.

왜 이렇게 빨라지는지에 대해서는 다음에 적도록 하겠습니다.

 

62697ms -> 70337ms -> 44373ms -> 16386ms -> 16116ms -> 9754ms -> 9154ms -> 7484ms -> 3586ms

 

 

 

MK-9 최적화(7) - ThreadPool

Thread를 이용한 병렬계산

 

앞서만든 로직을 그대로 Thread로 실행시키면 더욱빨라질겁니다.

 

@Override
    protected void innerExecute() {

        int availableThreadCount = Runtime.getRuntime().availableProcessors();
        int threadLoopCount = count / availableThreadCount;

        try (ExecutorService executor = Executors.newFixedThreadPool(threadLoopCount)) {
            for (int t = 0; t < availableThreadCount; t++) {
                executor.execute(() -> {
                    Random random = ThreadLocalRandom.current();
                    for (int i = 0; i < threadLoopCount; i++) {
                        long bitmask = generateNumbers(random);
                        int rank = evaluate(bitmask);
                        if (rank < 6) {
                            ranking[rank]++;
                        }
                    }
                });
            }
            executor.shutdown();
            executor.awaitTermination(Long.MAX_VALUE, TimeUnit.MICROSECONDS);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
// 986/ms 소요

 

62697ms -> 70337ms -> 44373ms -> 16386ms -> 16116ms -> 9754ms -> 9154ms -> 7484ms -> 3586ms -> 986ms

 

availableThreadCount 는 현재 가능한 스레드의 수

threadLoopCount 는 하나의 스레드가 반복문을 돌아야 되는 수를 나타낸겁니다.

 

ExecutorService 말고도

ThreadPoolExecutor a = new ThreadPoolExecutor();

이런식으로 core size와 max size를 설정하거나 다양한 옵션을 제공하는 Executor도 있지만 여기에선 별로 중요하지 않기 때문에 고정스레드풀을 사용했습니다.

 

아무래도 cpu의 모든 스레드를 순간적으로 사용하기에 CPU 점유율을 잠깐동안 100%를 차지하지만 뭐 잠깐이니까요.

 

 

 

결론

정말 의식의 흐름대로 복권을 검색했다가 여기까지 해버릴줄 몰랐습니다. 근데 최적화 하면서 나아지는게 보이니까 너무 재밌어서 못 놓겠더라구요.

115,411,832회의 로또 시뮬레이션 최적화 결과는 62.697초 -> 0.986초 로 단축에 성공했습니다.

완벽하지 않으니 언제든지 지적해주시면 감사하겠습니다.

끝으로 비트연산, 스레드, ThreadLocalRandom 에 대해서도 더 알게되어서 좋았습니당.

 

 

전체코드 공유

public interface LotterySimulator {
    void execute();
    void print();
}

public abstract class AbstractFixedCountLotterySimulator implements LotterySimulator {
    protected final int count;
    protected AbstractFixedCountLotterySimulator(int count) {
        this.count = count;
    }
    abstract protected void innerExecute();
    @Override
    public final void execute() {
        Times.timer(this::innerExecute);
    }
}

public class Times {
    public static void timer(Runnable runnable) {
        long start = System.currentTimeMillis();
        runnable.run();
        long end = System.currentTimeMillis();
        System.out.println(end - start + "/ms 소요");
    }
}

public class LotterySimulatorMK9 extends AbstractFixedCountLotterySimulator {

    private final long TARGET_MASK;
    private final long BONUS_TARGET_MASK;
    private final int[] ranking = new int[6];

    public LotterySimulatorMK9(int count, int[] targetNumbers, int bonusNumber) {
        super(count);
        this.TARGET_MASK = toBitmask(targetNumbers);
        this.BONUS_TARGET_MASK = 1L << bonusNumber;
    }

    private long toBitmask(int[] numbers) {
        long bitmask = 0;
        for (int number : numbers) {
            bitmask |= 1L << number;
        }
        return bitmask;
    }

    @Override
    protected void innerExecute() {

        int availableThreadCount = Runtime.getRuntime().availableProcessors();
        int threadLoopCount = count / availableThreadCount;

        try (ExecutorService executor = Executors.newFixedThreadPool(threadLoopCount)) {
            for (int t = 0; t < availableThreadCount; t++) {
                executor.execute(() -> {
                    Random random = ThreadLocalRandom.current();
                    for (int i = 0; i < threadLoopCount; i++) {
                        long bitmask = generateNumbers(random);
                        int rank = evaluate(bitmask);
                        if (rank < 6) {
                            ranking[rank]++;
                        }
                    }
                });
            }
            executor.shutdown();
            executor.awaitTermination(Long.MAX_VALUE, TimeUnit.MICROSECONDS);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private long generateNumbers(Random random) {
        long bitmask = 0L;
        int count = 0;

        while (count < 6) {
            long bit = 1L << random.nextInt(45) + 1;

            if ((bitmask & bit) == 0) {
                bitmask |= bit;
                count++;
            }
        }
        return bitmask;
    }

    private int evaluate(long bitmask) {
        int matchCount = Long.bitCount(TARGET_MASK & bitmask);

        if (matchCount < 2) return 6;
        if (matchCount == 6) return 1;

        boolean hasBonusNumber = (bitmask & BONUS_TARGET_MASK) != 0;

        return hasBonusNumber ? 8 - (matchCount + 1) : 8 - matchCount;
    }

    @Override
    public void print() {
        for (int i = 1; i < 6; i++) {
            System.out.printf("%d등 : %d명\n", i, ranking[i]);
        }
    }

}

public class Main2 {

    public static void main(String[] args) {
        int count = 115_411_832;
        int[] targetNumbers = {3,7,15,16,19,43};
        int bonusNumber = 21;
        LotterySimulator simulator = new LotterySimulatorMK9(count, targetNumbers, bonusNumber);
        simulator.execute();
        simulator.print();
    }

}

개요

Spring Security 에서 ThreadLocal에대해서 언급한적이 있어 글로 남겨봅니다.

 

ThreadLocal이란?

ThreadLocal은 Java에서 제공하는 클래스로, java.lang 패키지에 존재합니다.

각 스레드마다 독립적인 변수를 가질 수 있게 해주는 기능인데 쉽게 말해, ThreadLocal에 저장된 데이터는 해당 Thread만 접근할 수 있는 데이터 저장소라고 할 수 있습니다. 개인이 가지는 사물함이라고 생각하시면 쉽습니다.

 

 

 

동시성 문제

그럼 왜 ThreadLocal을 알아야 할까요? JAVA는 멀티쓰레딩 환경으로 한 메소드에 동시에 접근이 가능합니다. 그래서 항상 동시성문제에 신경써야하죠. 특히 Spring Container에서는 Bean으로 등록해 객체를 싱글톤으로 관리해 자원을 최소화 합니다. 하나의 객체를 여러명이 사용하면 문제가 생기기 마련입니다. 읽기 메소드는 멱등성을 보장받아 문제가 생기지않지만 수정, 생성, 삭제 등 데이터가 변경되었을 때 문제가 생기죠.

 

싱글톤패턴에 대해서 알고싶다면 아래 글을 읽어보세요!

 

 

자바(JAVA) - 싱글톤 패턴(Singleton Pattern)

싱글톤 패턴 싱글톤 패턴은 단 하나의 유일한 객체를 만들기 위한 코드 패턴입니다. 싱글톤 패턴을 따르는 클래스는 생성자가 여러번 호출되더라도 실제 생성되는 객체는 단 하나이고, 최초 생

tmd8633.tistory.com

 

 

자바(JAVA) - 싱글톤 패턴(Singleton Pattern)

싱글톤 패턴 싱글톤 패턴은 단 하나의 유일한 객체를 만들기 위한 코드 패턴입니다. 싱글톤 패턴을 따르는 클래스는 생성자가 여러번 호출되더라도 실제 생성되는 객체는 단 하나이고, 최초 생

tmd8633.tistory.com

 

public class BankAccount {
    private int balance = 0;
    
    // 동시성 문제가 발생하는 메서드
    public int transfer(int amount) {
        int currentBalance = balance;         // 현재 잔액 읽기
        balance = currentBalance + amount;    // 잔액 업데이트
        return balance;
    }
    
    public int getBalance() {
        return balance;
    }
}

 

가령 BankAccount 객체가 싱글톤으로 관리되고, 여러 사용자가 BankAccount를 사용한다고 했을때,

 

  1. 사용자A : bankAccount.transfer(1000)
  2. 사용자A 현재 잔액 읽음 : 현재 잔액 0
  3. 사용자B : bankAccount.transfer(1000)
  4. 사용자B 현재 잔액 읽음 : 현재 잔액 0
  5. 사용자A : 잔액 업데이트 - 결과 반환 1000
  6. 사용자B : 잔액 업데이트 - 결과 반환 1000

최종 잔액이 2000이 될 것으로 예상했지만 동시성 문제로 최종 잔액이 1000이 되었습니다.

 

 

 

ThreadLocal의 특징

 

  • 스레드 안전성: 각 스레드가 자신만의 독립된 변수를 가지므로, 동기화 없이도 스레드 안전성을 보장합니다.
  • 데이터 격리: 다른 스레드의 데이터에 접근할 수 없어 데이터 격리가 완벽하게 이루어집니다.
  • 성능: 동기화가 필요 없으므로, synchronized 키워드 사용 대비 성능상 이점이 있습니다.

 

 

ThreadLocal

public class ThreadLocal<T> {
    
    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }
    
    boolean isPresent() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        return map != null && map.getEntry(this) != null;
    }
    
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            map.set(this, value);
        } else {
            createMap(t, value);
        }
    }
    
    public void remove() {
        ThreadLocalMap m = getMap(Thread.currentThread());
        if (m != null) {
            m.remove(this);
        }
    }

    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
    
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

    static class ThreadLocalMap {

        static class Entry extends WeakReference<java.lang.ThreadLocal<?>> {
            Object value;

            Entry(java.lang.ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
    }
}

 

ThreadLocal은 ThreadLocalMap을 가지고있고 여기에서 key, value로 데이터를 보관합니다.

그리고 이때 get()메소드에서 Thread.currentThread()를 사용해 Thread를 꺼내고 그 ThreadLocalMap을 반환해서 가져오게됩니다.

 

 

이제 개념을 알았으니 어디에서 사용하고 있는지 간단하게 알아보겠습니다.

 

 

 

사용 사례

 

Spring Security

public class SecurityContextHolder {

    // ... 코드 생략

    private static void initializeStrategy() {
        if ("MODE_PRE_INITIALIZED".equals(strategyName)) {
            Assert.state(strategy != null, "When using MODE_PRE_INITIALIZED, setContextHolderStrategy must be called with the fully constructed strategy");
        } else {
            if (!StringUtils.hasText(strategyName)) {
                strategyName = "MODE_THREADLOCAL"; // ThreadLocal 전략이 Default
            }

            if (strategyName.equals("MODE_THREADLOCAL")) {
                strategy = new ThreadLocalSecurityContextHolderStrategy();
            } else if (strategyName.equals("MODE_INHERITABLETHREADLOCAL")) {
                strategy = new InheritableThreadLocalSecurityContextHolderStrategy();
            } else if (strategyName.equals("MODE_GLOBAL")) {
                strategy = new GlobalSecurityContextHolderStrategy();
            } else {
                try {
                    Class<?> clazz = Class.forName(strategyName);
                    Constructor<?> customStrategy = clazz.getConstructor();
                    strategy = (SecurityContextHolderStrategy)customStrategy.newInstance();
                } catch (Exception ex) {
                    ReflectionUtils.handleReflectionException(ex);
                }

            }
        }
    }
final class ThreadLocalSecurityContextHolderStrategy implements SecurityContextHolderStrategy {
    private static final ThreadLocal<Supplier<SecurityContext>> contextHolder = new ThreadLocal();
    
    // ... 코드 생략
}

 

 

 

 

Transaction

public class DataSourceTransactionManager extends AbstractPlatformTransactionManager implements ResourceTransactionManager, InitializingBean {

    // ... 코드 생략
    
    protected Object doGetTransaction() {
        DataSourceTransactionObject txObject = new DataSourceTransactionObject();
        txObject.setSavepointAllowed(this.isNestedTransactionAllowed());
        ConnectionHolder conHolder = (ConnectionHolder)TransactionSynchronizationManager.getResource(this.obtainDataSource());
        txObject.setConnectionHolder(conHolder, false);
        return txObject;
    }
    
    protected void doBegin(Object transaction, TransactionDefinition definition) {
    
        // ... 코드 생략
        if (txObject.isNewConnectionHolder()) {
            TransactionSynchronizationManager.bindResource(this.obtainDataSource(), txObject.getConnectionHolder());
        }    
        // ... 코드 생략
    }
}

public abstract class TransactionSynchronizationManager {
    private static final ThreadLocal<Map<Object, Object>> resources = new NamedThreadLocal("Transactional resources");
    private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations = new NamedThreadLocal("Transaction synchronizations");
    private static final ThreadLocal<String> currentTransactionName = new NamedThreadLocal("Current transaction name");
    private static final ThreadLocal<Boolean> currentTransactionReadOnly = new NamedThreadLocal("Current transaction read-only status");
    private static final ThreadLocal<Integer> currentTransactionIsolationLevel = new NamedThreadLocal("Current transaction isolation level");
    private static final ThreadLocal<Boolean> actualTransactionActive = new NamedThreadLocal("Actual transaction active");
    
    // ... 코드 생략
}

 

 

 

Hibernate

public class ThreadLocalSessionContext extends AbstractCurrentSessionContext {
    private static final CoreMessageLogger LOG = (CoreMessageLogger)Logger.getMessageLogger(CoreMessageLogger.class, ThreadLocalSessionContext.class.getName());
    private static final Class<?>[] SESSION_PROXY_INTERFACES = new Class[]{Session.class, SessionImplementor.class, EventSource.class, LobCreationContext.class};
    private static final ThreadLocal<Map<SessionFactory, Session>> CONTEXT_TL = ThreadLocal.withInitial(HashMap::new);
    
    // ... 코드 생략
}

 

 

 

 

ThreadLocal 주의사항

 

메모리 누수

ThreadLocal 사용 후에는 반드시 remove()를 호출하여 메모리 누수를 방지해야 합니다. 특히 스레드 풀을 사용하는 환경에서는 더욱 중요합니다.

 

try {
    // ThreadLocal 사용
    userContext.set(new UserContext());
    // 비즈니스 로직
} finally {
    // 반드시 삭제
    userContext.remove();
}

가능하면 try-with-resources 패턴을 사용해 자원을 반납하면 안전하게 사용할 수 있습니다.

 

성능 고려사항

  1. ThreadLocal은 각 스레드마다 별도의 메모리를 사용합니다.
  2. 많은 수의 ThreadLocal 변수를 사용하면 메모리 사용량이 증가할 수 있습니다.
  3. get()과 set() 연산은 매우 빠르지만, 너무 빈번한 접근은 피하는 것이 좋습니다.

 

static final 선언

static final로 선언하는걸 권장합니다. 위에 사용 사례를 보시면 모두 static final로 선언되어있는걸 보실 수 있습니다.

 

  • static 사용 이유
    • ThreadLocal 객체 자체는 모든 스레드가 공유해도 됩니다
    • 각 스레드마다 값을 따로 저장하는 것은 ThreadLocal의 내부 구현이 처리함
    • 불필요한 인스턴스 생성을 방지할 수 있음
    • 전역적으로 접근이 필요한 경우가 많음 (예: Transaction, Security 등)
  • final 사용 이유
    • ThreadLocal 인스턴스 자체는 변경될 필요가 없음
    • 한 번 생성된 후에는 참조가 변경되면 안됨
    • 실수로 ThreadLocal 참조를 바꾸는 것을 방지
    • 불변성을 보장하여 스레드 안전성을 높임

 

 

 

끝!

 

iOS Build를 하거나 앱스토어 등록을 위해 Achive 할때 Xcode 에서 generatedpluginregistrant.m 파일에 module not found 에러가 발생했습니다. module 중 가장 상위의 module을 찾을 수 없다 하여 모듈문제인줄 알고 오랫동안 찾아다녔네요.. 해결방법 공유합니다.

 

 

아래 순서대로 하나씩 따라해보면서 해결해봅시다.

 

 

1. Xcode로 Open한 Project가 Runner.xcworkspace 인지 확인

Runner.xcodeproj 로 열었다면 당장 끄고 같은 폴더 내에 Runner.xcworkspace로 열어서 다시 시도해봅시다

 

 

 

2. 프로젝트/iOS/Podfile 주석과 버전 확인

platform :ios, '12.0' 부분이 주석처리되었다면 활성화해주시고 Xcode - Runner - Build Settings - Deployment - iOS Deployment Target 과 버전을 일치시켜줍시다.

프로젝트/iOS/Podfile

 

Xcode - Runner - Build Settings - Deployment - iOS Deployment Targe

 

Runner - General - Minimum Deployments에 iOS 버전도 함께 확인하시기바랍니다. 최소버전이 더 높으면 말이 안되니까요

 

3. 다시 빌드

flutter clean
flutter pub get
cd ios
pod install

 

한번 다시 시도해봅시다.

 

 

4.  다른방법들

 

Podfile 새로 받아보자

# Uncomment this line to define a global platform for your project
platform :ios, '12.0'

# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'

project 'Runner', {
  'Debug' => :debug,
  'Profile' => :release,
  'Release' => :release,
}

def flutter_root
  generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
  unless File.exist?(generated_xcode_build_settings_path)
    raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
  end

  File.foreach(generated_xcode_build_settings_path) do |line|
    matches = line.match(/FLUTTER_ROOT\=(.*)/)
    return matches[1].strip if matches
  end
  raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
end

require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)

flutter_ios_podfile_setup

target 'Runner' do
  use_frameworks!
  use_modular_headers!

  flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
  target 'RunnerTests' do
    inherit! :search_paths
  end
end

post_install do |installer|
  installer.pods_project.targets.each do |target|
    flutter_additional_ios_build_settings(target)
  end
end

 

제꺼 Podfile 복사 붙혀넣기 하고 버전수정해서 다시 시도해봅시다.

 

 

Edit Scheme 에서 Build Configuration을 변경해보자

Xcode 중앙 상단에 Runner를 클릭해서 Edit Scheme... 를 클릭하고 Info - Build Configuration 설정을 확인해보자

Debug로 되어있다면 얼른 Release로 변경하고 다시 시도해봅시다.

 

 

 

다른 방법으로 해결했다면 댓글로 방법 알려주세요.

 

오류 메세지

Warning: Pub installs executables into $HOME/.pub-cache/bin, which is not on your path.
You can fix that by adding this to your shell's config file (.bashrc, .bash_profile, etc.):

  export PATH="$PATH":"$HOME/.pub-cache/bin"

 

 

dart pub global activate rename

bundle id를 변경하던 와중 오류가 발생했습니다. 이 오류를 해결해봅시다.

 

 

 

 

해결방법

open ~/.zshrc

터미널에서 위와같이 입력해줍니다.

 

 

export PATH="$PATH":"$HOME/.pub-cache/bin"

맨 밑줄에 이렇게 입력하고 저장해줍니다.

 

 

source ~/.zshrc

이렇게 입력해서 변경된 내용을 적용시켜줍니다.

 

 

 

끝!

개요

오늘은 Flutter로 도넛차트를 만들어보도록 하겠습니다.

 

 

디자인

 

 먼저 피그마로 도넛차트의 모양을 만들어보았습니다. 마음에 들어서 이 디자인을 사용하도록 하겠습니다.

 

 

 

 

DonutChart

class DonutChart extends StatefulWidget {
  
  final double radius;
  final double strokeWidth;
  final double total;
  final double value;
  Widget? child;

  DonutChart({
    super.key,
    this.radius = 100,
    this.strokeWidth = 20,
    required this.total,
    required this.value,
    this.child,
  });

  @override
  State<DonutChart> createState() => _DonutChartState();
}

class _DonutChartState extends State<DonutChart> {
  
  @override
  Widget build(BuildContext context) {
    return const Placeholder();
  }
  
}

 

먼저 도넛모양의 차트에 데이터는 위와같이 잡았습니다.

radius : 차트의 크기 default 100

strokeWidth : 차트의 width default 20

total : 전체 합

value : 표시할 값

 

 

다음은 애니메이션을 사용할것이기 때문에

SingleTickerProviderStateMixin

를 사용하고 AnimationController와 Animation<double> 을 만들어줍니다.

 

class _DonutChartState extends State<DonutChart> with SingleTickerProviderStateMixin {
  
  late AnimationController _controller;
  late Animation<double> _valueAnimation;
  
  @override
  void initState() {
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(seconds: 1)
    );
    
    super.initState();
  }
  
  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return const Placeholder();
  }

}

 

그리고  valueAnimation을 마저 구현해주고 AnimationController를 forward 해줍니다.

 

  @override
  void initState() {
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(seconds: 1)
    );

    final curvedAnimation = CurvedAnimation(
        parent: _controller,
        curve: Curves.fastOutSlowIn
    );

    _valueAnimation = Tween<double>(
      begin: 0,
      end: (widget.value / widget.total) * 360
    ).animate(_controller);

    _controller.forward();

    super.initState();
}

 

 

 

CustomPainter

class _DonutChartProgressBar extends CustomPainter {

  final double strokeWidth;
  final double valueProgress;

  _DonutChartProgressBar({super.repaint, required this.strokeWidth, required this.valueProgress});

  
  @override
  void paint(Canvas canvas, Size size) {
    
    Paint defaultPaint = Paint()
      ..color = Color(0xFFE1E1E1)
      ..strokeWidth = strokeWidth
      ..style = PaintingStyle.stroke
      ..strokeCap = StrokeCap.round;
    
    Paint valuePaint = Paint()
        ..color = Color(0xFF7373C9)
        ..strokeWidth = strokeWidth
        ..style = PaintingStyle.stroke
        ..strokeCap = StrokeCap.round;
    
    Offset center = Offset(size.width / 2, size.height / 2);
    
    canvas.drawArc(
      Rect.fromCircle(center: center, radius: size.width / 2), 
      math.radians(-90), 
      math.radians(360), 
      false, 
      defaultPaint
    );

    canvas.drawArc(
        Rect.fromCircle(center: center, radius: size.width / 2),
        math.radians(-90),
        math.radians(valueProgress),
        false,
        valuePaint
    );
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    return true;
  }

}

 

CustomPainter를 상속받아서 paint 해줍니다.

배경이 되는 defaultPaint 를 만들고, value 값을 표현하는 valuePaint를 만들어줍니다.

그리고 canvas에 그려주면 되는데 math는 

import 'package:vector_math/vector_math.dart' as math;

vector_math 를 임포트해주어야 사용할 수 있습니다.

 

drawArc의 첫번째 인자는 중심이 되는 지점을 표현한거고

두번째는 시작지점입니다. 3시방향이 0도이기때문에 12시 방향부터 시작하고싶어서 -90를 넣어주었습니다.

세번째는 마지막지점입니다. valueProgress 값을 사용해 외부에서 넣어주겠습니다.

네번째는 시작지점과 마지막지점을 중앙점과 이을건지 여부인데 true를 하게되면 Pie Chart가 됩니다. 저는 Donut Chart를 만들것이기 때문에 false를 해주었습니다.

마지막은 Paint를 넣어주면 됩니다.

 

shouldRepaint는 true로 하면됩니다.

 

자 이제 돌려보면

 

 

짜잔 완성했습니다. 어우 이거 만드는것보다 영상촬영하고 짤로 만드는게 더 어렵네요...

 

 

완성

DonutChart(
    radius: 50,
    strokeWidth: 10,
    total: 100,
    value: 85,
    child: Center(
      child: Text('85%',
        style: TextStyle(
          color: Colors.black54,
          fontWeight: FontWeight.w600,
          fontSize: 21
        ),
      ),
    ),
)

이렇게 완성했습니다. CustomPainter 나 Animation을 사용하는게 조금 쉽지 않았는데 만들고보니 뿌듯하네요

Stream API 개요

Java Stream API는 데이터 처리를 위한 강력한 도구이지만, 잘못 사용하면 오히려 성능이 저하될 수 있습니다. 이번 포스트에서는 Stream API의 효율적인 사용법과 병렬 스트림 활용 방법에 대해 알아보겠습니다.

 

 

 

병렬 스트림(Parallel Stream)

 

기본 사용법

// 순차 스트림
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream()
                .mapToInt(n -> n)
                .sum();

// 병렬 스트림
int parallelSum = numbers.parallelStream()
                        .mapToInt(n -> n)
                        .sum();

// 기존 스트림을 병렬로 변환
int anotherSum = numbers.stream()
                       .parallel()
                       .mapToInt(n -> n)
                       .sum();

 

병렬 스트림이 효과적인 경우

// 1. 데이터가 많은 경우
List<Integer> largeList = new ArrayList<>(1000000);
// 리스트 초기화...

long count = largeList.parallelStream()
                     .filter(n -> n % 2 == 0)
                     .count();

// 2. 독립적인 연산이 많은 경우
double average = largeList.parallelStream()
                         .mapToDouble(this::complexCalculation)
                         .average()
                         .orElse(0.0);

private double complexCalculation(int number) {
    // CPU 집약적인 계산
    return Math.sqrt(Math.pow(number, 2));
}

 

 

 

성능 최적화 전략

 

적절한 데이터 구조 선택

// ArrayList - 병렬 처리에 좋음
List<Integer> arrayList = new ArrayList<>();
arrayList.parallelStream()...

// LinkedList - 병렬 처리에 비효율적
List<Integer> linkedList = new LinkedList<>();
linkedList.stream()...  // 순차 처리 권장

// Array - 가장 효율적
int[] array = {1, 2, 3, 4, 5};
Arrays.stream(array).parallel()...

 

 

Unboxing 오버헤드 방지

// 비효율적인 방법 (boxing/unboxing 발생)
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream()
                .mapToInt(n -> n)  // unboxing
                .sum();

// 효율적인 방법
int[] primitiveNumbers = {1, 2, 3, 4, 5};
int sum = Arrays.stream(primitiveNumbers)
               .sum();

 

 

적절한 중간 연산 사용

// 비효율적인 방법
List<String> result = strings.stream()
                           .filter(s -> s.length() > 3)
                           .sorted()  // 전체 정렬 후 필터링
                           .limit(5)
                           .collect(Collectors.toList());

// 효율적인 방법
List<String> betterResult = strings.stream()
                                 .filter(s -> s.length() > 3)
                                 .limit(5)  // 먼저 개수 제한
                                 .sorted()  // 필요한 요소만 정렬
                                 .collect(Collectors.toList());

 

 

병렬 스트림 주의사항

 

상태 공유 피하기

// 잘못된 예 - 상태 공유로 인한 문제
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> result = new ArrayList<>();

numbers.parallelStream()
       .map(n -> n * 2)
       .forEach(result::add);  // 동시성 문제 발생

// 올바른 예
List<Integer> safeResult = numbers.parallelStream()
                                .map(n -> n * 2)
                                .collect(Collectors.toList());

 

 

순서 의존성 주의

// 순서에 의존적인 작업 - 병렬 처리 부적합
String result = strings.parallelStream()
                      .reduce("", (a, b) -> a + b);  // 순서 보장 안됨

// 올바른 방법
String betterResult = String.join("", strings);  // 더 효율적

 

 

주의점

  1. 데이터 크기가 작은 경우 순차 처리가 더 효율적
  2. 공유 상태 수정은 피하기
  3. 올바른 데이터 구조 선택
  4. 순서 의존성 고려
  5. 성능 측정 후 판단

 

마치며

Stream API의 효율적인 사용과 병렬 스트림의 적절한 활용은 애플리케이션의 성능을 크게 향상시킬 수 있습니다. 하지만 무조건적인 병렬 처리가 아닌, 상황에 맞는 적절한 선택이 중요합니다.

개요

Flutter에서 위젯의 크기를 제어하는 방법은 다양합니다. 그 중 가장 많이 사용되는 ConstrainedBox와 SizedBox의 차이점과 각각의 사용 사례에 대해 알아보겠습니다.

SizedBox의 이해

 

기본 사용법

// 고정 크기 지정
SizedBox(
  width: 100,
  height: 50,
  child: Container(
    color: Colors.blue,
  ),
)

// 간격 생성
SizedBox(height: 10)  // 세로 간격
SizedBox(width: 10)   // 가로 간격

// 최대 크기로 확장
SizedBox.expand(
  child: Container(
    color: Colors.blue,
  ),
)

 

SizedBox의 특징

  • 정확한 크기를 지정할 때 사용
  • 간단한 간격을 만들 때 유용
  • child가 없을 경우 빈 공간으로 사용
  • 성능상 가장 가벼운 위젯 중 하나

 

 

ConstrainedBox의 이해

 

기본 사용법

ConstrainedBox(
  constraints: BoxConstraints(
    minWidth: 100,
    maxWidth: 200,
    minHeight: 50,
    maxHeight: 100,
  ),
  child: Container(
    color: Colors.blue,
  ),
)

// 유용한 생성자들
ConstrainedBox(
  constraints: BoxConstraints.tightFor(width: 100, height: 100),
  child: Container(color: Colors.blue),
)

ConstrainedBox(
  constraints: BoxConstraints.loose(Size(200, 100)),
  child: Container(color: Colors.blue),
)

 

ConstrainedBox의 특징

  • 최소/최대 크기 제약 설정 가능
  • 자식 위젯의 크기를 유연하게 제어
  • 복잡한 레이아웃 제약 조건 설정 가능

 

 

실제 사용 예시

 

SizedBox 활용

class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text('First Item'),
        SizedBox(height: 16),  // 간격 추가
        Container(
          color: Colors.blue,
          child: SizedBox(
            width: 100,
            height: 100,
            child: Center(
              child: Text('Fixed Size'),
            ),
          ),
        ),
        SizedBox(height: 16),  // 간격 추가
        Text('Last Item'),
      ],
    );
  }
}

 

 

ConstrainedBox 활용

class FlexibleWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        ConstrainedBox(
          constraints: BoxConstraints(
            minHeight: 50,
            maxHeight: 200,
          ),
          child: Container(
            color: Colors.green,
            child: Text('This box can grow between 50 and 200'),
          ),
        ),
        ConstrainedBox(
          constraints: BoxConstraints.tightFor(
            width: double.infinity,
            height: 100,
          ),
          child: Card(
            child: Center(
              child: Text('Full width, fixed height'),
            ),
          ),
        ),
      ],
    );
  }
}

 

주요 차이점과 선택 기준

 

SizedBox 사용 시나리오

정확한 크기가 필요할 때

// 고정 크기 버튼
SizedBox(
  width: 200,
  height: 50,
  child: ElevatedButton(
    onPressed: () {},
    child: Text('Fixed Size Button'),
  ),
)

 

간단한 간격이 필요할 때

Column(
  children: [
    Text('Item 1'),
    SizedBox(height: 8),  // 작은 간격
    Text('Item 2'),
    SizedBox(height: 16), // 중간 간격
    Text('Item 3'),
    SizedBox(height: 24), // 큰 간격
    Text('Item 4'),
  ],
)

 

 

ConstrainedBox 사용 시나리오

유동적인 크기 제약이 필요할 때

ConstrainedBox(
  constraints: BoxConstraints(
    minWidth: 100,
    maxWidth: MediaQuery.of(context).size.width * 0.8,
    minHeight: 50,
  ),
  child: Container(
    padding: EdgeInsets.all(16),
    child: Text('This box adapts to content'),
  ),
)

 

반응형 레이아웃 구현 시

ConstrainedBox(
  constraints: BoxConstraints(
    maxWidth: 600,  // 태블릿/데스크톱에서 최대 너비 제한
  ),
  child: ListView(
    children: [
      // 리스트 아이템들
    ],
  ),
)

 

 

성능 고려사항

 

SizedBox

// 권장: 간단하고 효율적
SizedBox(width: 100, height: 100)

// 비권장: 불필요한 중첩
Container(
  width: 100,
  height: 100,
)

 

ConstrainedBox

// 권장: 필요한 경우만 사용
ConstrainedBox(
  constraints: BoxConstraints(maxWidth: 200),
  child: Text('Limited width text'),
)

// 비권장: 불필요한 제약 사용
ConstrainedBox(
  constraints: BoxConstraints.tightFor(width: 100, height: 100),
  child: Container(),  // SizedBox를 사용하는 것이 더 효율적
)

 

 

마치며

  • SizedBox는 정확한 크기나 간격이 필요할 때 사용
  • ConstrainedBox는 유연한 크기 제약이 필요할 때 사용
  • 성능을 고려할 때는 가능한 한 간단한 위젯을 선택
  • 레이아웃의 목적과 요구사항에 따라 적절한 위젯 선택이 중요

개요

기본적으로 부모 위젯에서 자식 위젯의 메소드를 호출할 수 있는 방법은 따로 없습니다. 하지만 불가능한것은 아니죠. 오늘은 그것에 대해서 알아보도록 하겠습니다.
 
 
 
 

Controller

부모 - 자식 위젯간에 Controller를 하나 두는 방법입니다. 부모 위젯에서 Controller를 생성하고 자식 위젯에게 전달해 메소드를 주입받는 방식이죠.
 

class TimerController {

  late Function() start;
  late Function() stop;

}

 
먼저 이렇게 Controller를 만듭니다.
 

class MainPage extends StatefulWidget {
  const MainPage({super.key});
  
  @override
  State<MainPage> createState() => _MainPageState();
}

class _MainPageState extends State<MainPage> {

  final TimerController _timerController = TimerController();

  @override
  Widget build(BuildContext context) {
    return CustomTimerWidget(
      controller: _timerController,
    );
  }
}

 
부모위젯에서 자식위젯에 TImerController를 주입해줍니다.
 

class CustomTimerWidget extends StatefulWidget {

  TimerController? controller;
  CustomTimerWidget({super.key, this.controller});

  @override
  State<CustomTimerWidget> createState() => _CustomTimerWidgetState();
}

class _CustomTimerWidgetState extends State<CustomTimerWidget> {
  
  void start() {
    print('timer start');
  }
  void stop() {
    print('timer stop');
  }
  _timerInit() {
    widget.controller?.start = start;
    widget.controller?.stop = stop;
  }
  
  @override
  void initState() {
    _timerInit();
    super.initState();
  }
  
  @override
  Widget build(BuildContext context) {
    return const Placeholder();
  }
}

 
자식 위젯에서 initState 시점에 부모로 부터 받은 controller에 Function을 넣어줍니다.
이제 부모위젯에서 자식 위젯의 메소드를 사용할 수 있게되었습니다.
 
 
이 방법은 flutter내에서 정말 많이 사용하고 있는 패턴같습니다. PageView, ListView 등등 다양한곳에서 Controller를 받는걸 볼 수 있습니다. 이 방법말고 GlobalKey를 이용해 자식위젯의 메소드를 사용할 수 는 있지만 같은 파일(.dart) 내에 존재하지 않는다면 사용 할 수 없다는 큰 단점이 있고, GlobalKey를 많이 사용할 수록 복잡해지는 단점이있어서 위와 같은 방식을 추천드립니다.

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':app:mergeReleaseResources'.
> Multiple task action failures occurred:
   > A failure occurred while executing com.android.build.gradle.internal.res.Aapt2CompileRunnable
      > Android resource compilation failed
        ERROR:/Users/user/app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: AAPT: error: file failed to compile.

   > A failure occurred while executing com.android.build.gradle.internal.res.Aapt2CompileRunnable
      > Android resource compilation failed
        ERROR:/Users/user/app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: AAPT: error: file failed to compile.

   > A failure occurred while executing com.android.build.gradle.internal.res.Aapt2CompileRunnable
      > Android resource compilation failed
        ERROR:/Users/user/app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: AAPT: error: file failed to compile.

   > A failure occurred while executing com.android.build.gradle.internal.res.Aapt2CompileRunnable
      > Android resource compilation failed
        ERROR:/Users/user/app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: AAPT: error: file failed to compile.

   > A failure occurred while executing com.android.build.gradle.internal.res.Aapt2CompileRunnable
      > Android resource compilation failed
        ERROR:/Users/user/app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: AAPT: error: file failed to compile.


* Try:
> Run with --stacktrace option to get the stack trace.
> Run with --info or --debug option to get more log output.
> Run with --scan to get full insights.

* Get more help at https://help.gradle.org

BUILD FAILED in 9s
Running Gradle task 'assembleRelease'...                           10.2s
Gradle task assembleRelease failed with exit code 1

 

 

앱 아이콘을 변경하고 이런 오류가 발생했습니다.

이미지 파일에도 문제가 없는데 말이죠...

 

앱 아이콘은 반드시 png 파일로 되어있어야 합니다.

 

그런데 이름만 png이지 실제 확장자는 JPEG로 되어있더군요

 

 

jpeg -> png 로 변환하고 다시 build하니 잘 되었습니다.

플러터 앱 아이콘 변경하는 법

 

flutter_launcher_icons 패키지를 통해 쉽게 바꿀 수 있지만 패키지를 사용하기 전에 직접 바꾸는 법을 알아보도록 하겠습니다.

 

flutter_launcher_icons | Dart package

A package which simplifies the task of updating your Flutter app's launcher icon.

pub.dev

 

 

 

 

아이콘 만들기

 

 

App Icon Generator

 

www.appicon.co

 

 

이미지를 업로드하고 원하는 플랫폼을 선택한 후에 Generate를 누르면

 

 

이렇게 파일이 생성됩니다.

 

 

 

 

안드로이드

android/app/src/main/res 에 들어가면

이런 데이터들이 있는데 이곳에다가 아까 다운받은 아이콘안에 android 폴더 내부데이터를 모두 이곳으로 옮겨줍니다.

 

 

 

 

iOS

ios/Runner/Assets.xcassets 폴더에 Assets.appiconset 폴더가 있는데 이것도 마찬가지로 다운받은 폴더에 Assets.appiconset 를 덮어쓰기 해주시면됩니다.

 

iOS

 

끝!

+ Recent posts