개요

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

 

끝!

플러서 앱 이름 바꾸는 법

 

 

안드로이드

app/src/main/AndroidManifest.xml 에 들어가서

<application
    android:label="앱 이름"

label에 이름을 변경하면 됩니다.

 

 

iOS

ios/Runner/Info.plist 에 들어가서

<key>CFBundleDisplayName</key>
<string>앱 이름</string>

 

CFBundleDisplayName 을 찾고 아래 string 에 이름을 변경하면 됩니다.

 

iOS

 

+ Recent posts