Spring Security

Spring Security는 인증, 인가 및 일반적인 공격에 대한 보호를 제공하는 프레임워크 입니다. 서비스를 개발할때 필수적으로 구현해야하는것이 로그인, 회원가입과 권한인데요. 이를 Spring Security를 통해 쉽게 해결할 수 있습니다. 다만 이 프래임워크가 워낙 방대하고 구조를 이해하고 있지않으면 활용하기 쉽지 않습니다. 하나의 게시물로 모든 Security 기능에 대해서 소개하지 못하므로, 이 글에서는 프레임워크에 대해 기본개념과 주요기능에 대해서 소개하고 각 기능에 대한 상세한 설명은 다음 글에서 이어가도록 하겠습니다.

 

이 글은 Spring Boot 3.4.0, Spring Security 6.4.1 환경에서 제작되었습니다.

 

 

 

들어가기전에

Security를 공부하기전에 짧게 기본개념을 잡고 가겠습니다. 이해하는데 도움이 될겁니다.

 

 

1. Filter

Spring Security는 Servlet Filter를 기반으로 동작합니다.

Client - Filter - DispatcherServlet - Interceptor - Controller

 

 

2. 용어

용어 설명
Authentication 인증
  • 사용자가 자신이 주장하는 사람이 맞는지 확인하는 과정
  • '신원 확인'의 과정
    예) 공항에서 여권으로 신원을 확인하는 것
Authorization 인가
  • 인증된 사용자가 특정 리소스에 접근할 수 있는 권한이 있는지 확인하는 과정
  • '권한 확인'의 과정
    예) 회사에서 직급에 따라 접근할 수 있는 시스템이 다른 것
Principal 접근주체
  • 시스템에 접근하는 사용자, 디바이스 또는 다른 시스템 등의 주체
  • 인증 이후 SecurityContext에 저장되는 인증된 사용자의 정보
    예) 사원증을 소지한 직원
Role 권한
  • 특정 리소스에 대한 접근 권한을 그룹화한 것
  • 사용자에게 부여될 수 있는 권한의 집합
    예) 회사에서의 직급 (사원, 대리, 과장 등)

 

용어 간의 관계

1. Authentication -> Principal 생성
   (인증 성공 시 Principal 정보가 생성됨)

2. Principal -> Role 부여
   (인증된 사용자에게 역할이 부여됨)

3. Role -> Authorization 결정
   (부여된 역할에 따라 권한이 결정됨)

 

 

3. SecurityContextHolder

SecurityContextHolder는 인증된 사용자의 정보를 저장하는 곳입니다.

 

 

4. Filter, Authentication, Authorization

Security에서 크게 Filter, Authentication, Authorization 으로 나눌 수 있습니다. 아키텍처를 볼 때, 이렇게 3개로 구분해서 보면 이해가 조금 더 쉬울 수 있습니다.

 

 

 

 

 

 

1. 아키텍처

 

출처 : https://docs.spring.io/spring-security/reference/servlet/authentication/architecture.html

 

시큐리티 공식문서에서 가져온 아키텍처입니다. 이 사진을 보고 이해해야할것은 아래와 같습니다.

 

  1. SecurityFilterChain 은 Filter에서 동작한다.
  2. Authentication을 통해 인증과정을 수행한다.
    인증에 성공하면 SecurityContextHolder에 인증된 사용자의 정보를 담는다.
    인증에 실패하면 SecurityContextHolder가 비워집니다.(cleared out)

 

 

2. Filter

 

2-1.  DelegatingFilterProxy

 

서블릿 컨테이너와 Spring의 ApplicationContext 사이의 연결점입니다. 서블릿 컨테이너는 자체 표준을 사용하여  필터 인스턴스를 등록할 수 있지만 Bean에 대해서는 알지 못합니다. 그래서 DelegatingFilterProxy 에 Bean 등록을 위임하도록 합니다.

 

한줄정리 : DelegatingFilterProxy는 Servlet Container - Spring ApplicationContext를 연결하는 역할을 한다!

 

 

2-2. FilterChainProxy과  SecurityFilterChain

 

  1. FilterChainProxy는 보안필터의 시작점입니다.
  2. 요청 URL에 따라(RequestMatcher) 적절한 SecurityFilterChain을 선택할 수 있도록 합니다.
  3. Spring Security의 HttpFirewall을 적용하여 특정 유형의 공격으로부터 애플리케이션을 보호합니다.
  4. FilterChianProxy는 bean 이기때문에 DelegatingFilterProxy로 감싸져있습니다.

 

 

요청 URL에 따라 SecurityFilterChain에서 Filter를 선택

 

 

 

2-3. SecurityFilterChain 내부 Filter

 

spring-security/config/src/main/java/org/springframework/security/config/annotation/web/builders/FilterOrderRegistration.java at

Spring Security. Contribute to spring-projects/spring-security development by creating an account on GitHub.

github.com

SecurityFilterChain에는 여러 Filter가 존재하는데 그 Filter의 순서는  FilterOrderRegistration.class 를 통해 확인할 수 있습니다. Filter의 순서를 외울필요는 없지만 어느 정도 흐름은 알고있는게 좋습니다. 

각 Filter의 용도와 사용법은 다음 글에서 알아보도록하겠습니다.

 

 

 

 

3.  Authentication (인증)

사용자 정보를 가지고 인증을 하는 과정입니다.

 

3-1. SecurityContextHolder

 

 

  • SecurityContextHolder는 인증된 사용자의 정보를 담고있는 Container입니다. Spring Security는 SecurityContextHolder가 어떻게 채워지는지 묻지않습니다. 그저 값이 포함되어있으면 인증된 사용자로 인식합니다. 즉, SecurityContextHolder가 비어있으면 인증되지않은 사용자, 비어있지않으면 인증된 사용자로 인식됩니다.
  • ThreadLocal을 사용하여 보안정보를 저장합니다. 즉, SecurityContext는 동일한 스레드내에서는 어디서든 보안 정보를 쉽게 접근 가능합니다. 요청 처리가 끝나면 반드시 쓰레드의 정보를 지워야 하지만 FilterChainProxy가 이 청소를 자동으로 해주기때문에 매우 안전하게 사용할 수 있습니다.
  • ThreadLocal에 대해 안전하지않은 어플리케이션에 대해서는 ThreadLocal 설정을 변경할 수 있습니다.
 

[JAVA] ThreadLocal에 대해서 알아보자

개요Spring Security 에서 ThreadLocal에대해서 언급한적이 있어 글로 남겨봅니다. ThreadLocal이란?ThreadLocal은 Java에서 제공하는 클래스로, java.lang 패키지에 존재합니다.각 스레드마다 독립적인 변수를

tmd8633.tistory.com

 

3-2. SecurityContext

인증된 객체(Authentication)를 보관하는 역할을 합니다. SecurityContextHolder로 부터 가져올 수 있습니다.

 

 

3-3. Authentication

현재 접근하는 사용자의 정보와 권한을 담은 인터페이스 입니다. isAuthenticated() 메소드로부터 인증되었는지 확인할 수 있습니다.

 

용어 설명
principal 아이디/비밀번호로 인증할때 사용자를 식별하는 역할을 합니다. UserDetails의 인스턴스로 사용됩니다.
credentials 비밀번호로 사요됩니다. 사용자가 인증된 후에는 비밀번호가 지워집니다.
authorities 사용자에게 부여된 권한들입니다. GrantedAuthority의 List형식으로 되어있습니다.

 

GrantedAuthority

주체에게 부여된 권한입니다. "ROLE_ADNATIOR", "ROLE_HR_SUPERVISOR" 과 같은 '역할'로 사용됩니다.

사용자 아이디/비밀번호 기반 인증을 사용할 때, UserDetailsService에 의해 로드됩니다.

 

 

3-4. AuthenticationManager

AuthenticationManager는 Spring Security의 필터가 인증을 수행하는 방식을 정의하는 인터페이스입니다.

AuthenticationManager는 인터페이스이므로 따로 구현할 수 있지만 기본적으로 ProviderManager가 구현되어있습니다.

 

 

3-5. AuthenticationProvider

실제 인증에 대한 부분을 담당합니다. 인증전의 Authentication 객체를 받아서 인증이 완료된 Authentication 객체를 반환합니다.

 

 

3-6. UserDetailService

UserDetailService를 implements해서 DB 데이터를 주입하여 UserDetails 를 반환하게 합니다.

public interface UserDetailsService {
    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

 

 

3-7. UserDetails

인증된 사용자로써 사용될 인터페이스 입니다. implements해서 Custom해야합니다.

public interface UserDetails extends Serializable {
    Collection<? extends GrantedAuthority> getAuthorities();

    String getPassword();

    String getUsername();

    default boolean isAccountNonExpired() {
        return true;
    }

    default boolean isAccountNonLocked() {
        return true;
    }

    default boolean isCredentialsNonExpired() {
        return true;
    }

    default boolean isEnabled() {
        return true;
    }
}

 

 

4. Authorization (인가)

인증된 정보를 가지고 접근에 대한 허용여부를 결정하는 과정입니다. GrantedAuthority에 String getAuthority() 메소드가 사용됩니다.

기본적인 규칙으로 'ROLE_' 접두사가 포함됩니다. 즉, "USER"라는 권한이 있는 경우 GrantedAuthority#getAuthority 를 사용하면 "ROLE_USER" 권한을 찾습니다.

 

'ROLE_' 접두사를 변경할 수 있습니다.

@Bean
static GrantedAuthorityDefaults grantedAuthorityDefaults() {
    return new GrantedAuthorityDefaults("MYPREFIX_");
}
// static 필수!!

 

 

 

 

5. 인증처리 과정

 

 

5-1. HTTP 요청

사용자가 로그인 요청을 합니다.

 

5-2 AuthenticationFilter 인증

SecurityFilterChain에서 로그인을 담당하는 UsernamePasswordAuthenticationFilter 에서 인증을 처리합니다.

 

5-3. UsernameAuthenticationToken 발급

아이디와 비밀번호를 통해 토큰을 발급하고 AuthenticationManager로 토큰을 보냅니다.

 

5-4. AuthenticationProvider 인증

인증된 객체를 UserDetailsService에게 넘겨줍니다.

 

5-5. UserDetails 생성

전달된 인증된 객체를 DB에서 조회하여 데이터를 UserDetails 반환

 

5-6. AuthenticationProvider

인증에 성공하면 AuthenticationProvider에서 AuthenticationManager에게 인증에 성공한 객체를 반환

 

5-7. AuthenticationManager

인증에 성공한 객체를 AuthentifacionFIlter에 전달

 

5-8. SecurityContextHolder

인증에 성공한 객체는 SecurityContextHolder에 저장

 

 

 

6. 설정

SecurityFilterChain 에 어떤 기능을 사용할지 설정하는 부분입니다. 이 부분에서는 Filter의 기능을 사용자의 API에 맞게 수정할 수 있고, 해당 Filter의 전과 후에 Custom Filter를 넣는 등 여러 기능을 설정할 수 있습니다.

 

6-1. 기본설정

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {

        http.csrf(Customizer.withDefaults())
            .httpBasic(Customizer.withDefaults())
            .formLogin(Customizer.withDefaults())
            .authorizeHttpRequests(authorize -> authorize
                .anyRequest().authenticated()
            );

        return http.build();
    }

}

 

메소드 필터 설명
csrf CsrfFilter csrf에 대한 설정
httpBasic BasicAuthenticationFilter HTTP Basic Authentication에 대한 설정
formLogin UsernamePasswordAuthenticationFilter form login에 대한 설정
authorizeHttpRequests AuthorizationFilter 권한에 대한 설정

 

위의 4개의 메소드뿐만아니라 수많은 filter에 대한 설정을 할 수 있습니다.

 

Customizer.withDefaults() 는 Security 기본설정에 따른다는 메소드입니다.

 

CSRF에 대해서 이해하기

 

[CS][Spring Security] CSRF란?

CSRF란?CSRF Cross-Site Request Forgery의 약자로 인증된 사용자의 권한을 악용하여 해당 사용자가 의도하지 않은 요청을 웹사이트에 전송하는 공격 기법입니다. 공격자는 사용자가 이미 인증된 상태를

tmd8633.tistory.com

 

 

6-2. Filter 추가

http.

// atFilter를 filter로 변경
addFilterAt(Filter filter, Class<? extends Filter> atFilter)

// beforeFilter 전에 filter를 추가
addFilterBefore(Filter filter, Class<? extends Filter> beforeFilter)

// afterFilter 후에 filter를 추가
addFilterAfter(Filter filter, Class<? extends Filter> afterFilter)
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        // ...
        .addFilterAfter(new TenantFilter(), AnonymousAuthenticationFilter.class); 
    return http.build();
}

 

 

Filter를 Bean으로 등록

Filter를 Bean으로 선언하면 Spring Container에 등록되면서 Filter가 2번 호출 될 수 있습니다. 이를 방지하기 위해 setEnabled(false) 로 설정해주어야 합니다.

 

@Bean
public FilterRegistrationBean<TenantFilter> tenantFilterRegistration(TenantFilter filter) {
    FilterRegistrationBean<TenantFilter> registration = new FilterRegistrationBean<>(filter);
    registration.setEnabled(false);
    return registration;
}

이렇게 하면 HttpSecurity 에 추가한 것만 유효하게 됩니다.

 

 

 

참고자료

공식문서 : https://docs.spring.io/spring-security/reference/servlet/architecture.html

 

 

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로 변경하고 다시 시도해봅시다.

 

 

 

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

플라이웨이트 패턴이란?

플라이웨이트 패턴은 많은 수의 유사한 객체를 효율적으로 관리하기 위한 구조적 디자인 패턴입니다. 객체의 내부 상태를 공유함으로써 메모리 사용을 최적화합니다. 코드를 보면서 이해해봅시다.

 

 

재사용 가능 여부

플라이웨이트 패턴은 자원의 재사용 여부를 먼저 따져봐야합니다. 같은 자원을 공유해도 문제가 없는 데이터를 재사용함으로써 메모리를 절약할 수 있으니까요.

예제로 전쟁시뮬레이션 게임을 만든다고 가정해보겠습니다. 하나의 객체를 만들때 텍스쳐, 사운드, 폴리곤, 객체의 좌표가 사용된다고 해보겠습니다.

 

 

 

플라이웨이트 패턴 적용 전

먼저 메모리 사용량을 표시하기 위해 메모리 클래스를 하나 만들겠습니다.

public class Memory {

    public static int size = 0;

    public static void print() {
        System.out.println("메모리 사용량 : " + size + "MB");
    }
}

 

 

public class Soldier {

    private final int size = 10;
    private final String texture;
    private final String sound;
    private final int polygons;

    private double positionX;
    private double positionY;

    public Soldier(String texture, String sound, int polygons, double positionX, double positionY) {
        this.texture = texture;
        this.sound = sound;
        this.polygons = polygons;
        this.positionX = positionX;
        this.positionY = positionY;

        // 메모리 사용량 증가
        Memory.size += size;
    }

}

 

public class Field {

    // 지형 타일 크기
    public static final int CANVAS_SIZE = 1000;

    public void render(String texture, String sound, int polygons, double positionX, double positionY) {
        Soldier soldier = new Soldier(texture, sound, polygons, positionX, positionY);
        System.out.println("병사 생성 : x : " + positionX + ", " + " y : " + positionY);
    }
}

 

 

    public static void main(String[] args) {
        Field field = new Field();

        for (int i = 0; i < 1000; i++) {
            field.render(
                "robe.png",
                "solider.wav",
                200,
                Field.CANVAS_SIZE * Math.random(),
                Field.CANVAS_SIZE * Math.random()
            );
        }

        Memory.print(); // 메모리 사용량 : 10000MB
    }

 

Solider 1000명을 Field에 rendering하는 코드를 간단하게 작성해보았습니다. 이때 무려 메모리를 10000mb나 사용되었네요. 만약 병사의 수가 증가한다면 기하급수적으로 메모리를 사용할 것입니다. 이 코드를 플라이웨이트 패턴을 이용해 메모리 사용량을 줄여보겠습니다.

 

 

플라이웨이트 패턴 적용 후

public class Soldier {

    private final int size = 2;

    private SoliderModel model;
    private double positionX;
    private double positionY;

    public Soldier(SoliderModel model, double positionX, double positionY) {
        this.model = model;
        this.positionX = positionX;
        this.positionY = positionY;

        // 메모리 사용량 증가
        Memory.size += size;
    }

}
public class SoliderModel {

    private final int size = 8;
    private final String texture;
    private final String sound;
    private final int polygons;

    public SoliderModel(String texture, String sound, int polygons) {
        this.texture = texture;
        this.sound = sound;
        this.polygons = polygons;

        Memory.size += size;
    }

}

 

Solider에서 재사용이 가능한 텍스쳐, 사운드, 폴리곤의 수를 SoliderModel 클래스로 새로 만들었습니다.

메모리 사용량은 SoliderModel = 8, Solider = 2 로 분배되었습니다.

 

public class SoliderModelFactory {

    private static final Map<String, SoliderModel> cache = new HashMap<>();

    public static SoliderModel getModel(String texture, String sound, int polygons) {
        String key = texture + sound + polygons;
        return cache.computeIfAbsent(key, k -> new SoliderModel(texture, sound, polygons));
    }
    
    public static void clear() {
        cache.clear();
    }
}

 

Factory를 만들고 한번 만들었던 모델데이터는 이곳에서 재사용됩니다.

 

public class Field {

    // 지형 타일 크기
    public static final int CANVAS_SIZE = 1000;

    public void render(String texture, String sound, int polygons, double positionX, double positionY) {
        SoliderModel model = SoliderModelFactory.getModel(texture, sound, polygons);
        Soldier soldier = new Soldier(model, positionX, positionY);
        System.out.println("병사 생성 : x : " + positionX + ", " + " y : " + positionY);
    }
}

필드에서 SoliderModelFactory.getModel 로 모델데이터를 가져와 Solider를 만들어 렌더링합니다.

 

    public static void main(String[] args) {
        Field field = new Field();

        for (int i = 0; i < 1000; i++) {
            field.render(
                "robe.png",
                "solider.wav",
                200,
                Field.CANVAS_SIZE * Math.random(),
                Field.CANVAS_SIZE * Math.random()
            );
        }

        Memory.print(); // 메모리 사용량 : 2008MB
        SoliderModelFactory.clear();
    }

메모리 사용량을 출력해본 결과 2008MB 가 나왔습니다. 플라이웨이트 적용전보다 무려 80% 감소되었습니다. 반복이 많아질 수록 경량화될겁니다.

 

 

주의점

렌더링이 끝났다면 factory를 초기화해줄 필요가 있습니다. 관리되고있는 인스턴스는 GC에서 제거하지 않기 때문에 메모리 누수가 발생할 수 있습니다.  참고로 이런 캐싱방식은 StringConstantPool과 같은 개념이라고 할 수 있습니다.

 

 

[JAVA] String의 불변성(immutability)

들어가며JAVA를 처음 접할 때, String은 불변객체라고 배우고 지나갔습니다. 하지만 실제 String을 다루다보면 어째서 불변객체인지 의문이 들곤합니다.  String str = "apple"; str = "banana";왜냐하면, 재할

tmd8633.tistory.com

String Constant Pool과 String 불변성에 대해서 더 알고싶다면 해당 게시물을 읽어보시는걸 추천드립니다.

 

 

마치며

플라이웨이트 패턴은 메모리 사용량을 크게 줄일 수 있는 강력한 도구입니다. 하지만 모든 상황에서 적합한 것은 아니며, 다음과 같은 상황에서 고려해볼 만합니다:

  1. 애플리케이션에서 많은 수의 유사한 객체를 사용할 때
  2. 객체의 상태를 내부와 외부로 명확히 구분할 수 있을 때
  3. 메모리 사용량이 중요한 제약 조건일 때

 

 

 

 

 

복합 패턴 개요

복합 패턴은 객체들을 트리 구조로 구성하여 부분-전체 계층구조를 구현하는 패턴입니다. 이 패턴을 사용하면 클라이언트에서 개별 객체와 복합 객체를 동일하게 다룰 수 있습니다. 오늘은 이 패턴에 대해서 공부해보겠습니다.

출처 : https://ko.wikipedia.org/wiki/%EC%BB%B4%ED%8F%AC%EC%A7%80%ED%8A%B8_%ED%8C%A8%ED%84%B4

 

 

 

뭐하는 패턴이지?

위의 구조를 보면 당최 뭐하는 패턴인지 감이 안잡힐 수 있습니다. 간단하게 말하면 복합객체와 단일객체를 동일한 타입을 취급해서 단순화시키는 것인데요. 예를들어 폴더 - 파일의 구조를 가진다고 했을때, 폴더와 파일을 하나의 객체로 동일하게 취급해서 구조를 단순화 시키는 것입니다. 폴더안에는 파일이 들어갈 수 있고, 폴더안에는 폴더가 들어갈 수 있는 구조를 생각했을 때 이걸 효과적으로 단일화 시킬 수 있다는 뜻이죠. 코드를 보면서 이해해보도록 하겠습니다.

 

 

 

복합 패턴의 구조

위의 사진에서 Leaf는 단일객체, Composite는 복합객체를 뜻합니다. 우리는 Leaf -> 파일, Composite -> 폴더로 치환해서 코드를 구현해보겠습니다.

 

public interface FileComponent {

    int getSize();
    String getTitle();

}

 

public class File implements FileComponent {

    private final String title;
    private final int size;

    public File(String title, int size) {
        this.title = title;
        this.size = size;
    }

    @Override
    public int getSize() {
        return size;
    }

    @Override
    public String getTitle() {
        return title;
    }
}

 

public class Folder implements FileComponent {

    private final String title;
    private final List<FileComponent> files = new ArrayList<>();

    public Folder(String title) {
        this.title = title;
    }

    @Override
    public int getSize() {
        int size = 0;
        for (FileComponent file : files) {
            size += file.getSize();
        }
        return size;
    }

    @Override
    public String getTitle() {
        return title;
    }

    public void add(FileComponent file) {
        files.add(file);
    }

    public void remove(FileComponent file) {
        files.remove(file);
    }

}

 

FileComponent interface를 만들고 파일과 폴더에서 구현했습니다.

 

 

    public static void main(String[] args) {
        Folder folder1 = new Folder("폴더1");
        folder1.add(new File("파일1", 300));

        Folder folder2 = new Folder("폴더2");
        folder2.add(new File("파일2", 1000));
        folder2.add(new File("파일3", 500));


        Folder folder3 = new Folder("폴더2");


        folder1.add(folder2);
        folder1.add(folder3);

        System.out.println("folder1.getTitle() = " + folder1.getTitle()); // 폴더1
        System.out.println("folder2.getSize() = " + folder1.getSize()); // 1800
    }

 

폴더1을 만들고 파일1을 넣었습니다.

폴더2를 만들고 파일2와 파일3을 넣었습니다.

그리고 아무것도 들어있지않은 폴더3을 만들었습니다.

 

마지막으로 폴더1에 폴더2와 폴더3을 넣었습니다.

 

이제 folder1의 title과 size를 출력해보면 내부에 들어있는 모든 파일의 크기가 출력됩니다.

 

 

 

복합 패턴의 장단점

장점

  1. 클라이언트 코드를 단순화
  2. 새로운 종류의 구성요소를 쉽게 추가
  3. 전체-부분 관계를 표현하기 용이

단점

  1. 설계가 지나치게 일반화될 수 있음
  2. 제약사항을 추가하기 어려움

 

 

 

복합패턴을 이용해 메뉴시스템 구현

복합패턴에 대해서 배웠으니 이 패턴을 활용해서 메뉴시스템을 구현해보겠습니다.

 

public abstract class MenuComponent {

    protected String name;
    protected String description;

    public MenuComponent(String name, String description) {
        this.name = name;
        this.description = description;
    }

    public abstract void print();
}
public class MenuItem extends MenuComponent {

    private int price;

    public MenuItem(String name, String description, int price) {
        super(name, description);
        this.price = price;
    }

    @Override
    public void print() {
        System.out.println(name + ": " + description + " - " + price + "원");
    }
}
public class Menu extends MenuComponent {
    
    private List<MenuComponent> menuComponents = new ArrayList<>();

    public Menu(String name, String description) {
        super(name, description);
    }

    public void add(MenuComponent menuComponent) {
        menuComponents.add(menuComponent);
    }

    @Override
    public void print() {
        System.out.println("\n" + name + ": " + description);
        System.out.println("--------------------");

        for (MenuComponent menuComponent : menuComponents) {
            menuComponent.print();
        }
    }
}

 

폴더 - 파일 구조와 동일하게 구현했습니다. 조금 다른게 있다면 interface -> abstract가 변경된게 있습니다만 무시해도됩니다.

 

    public static void main(String[] args) {
        // 메인 메뉴
        Menu mainMenu = new Menu("메인 메뉴", "메뉴를 선택해주세요");

        // 커피 메뉴
        Menu coffeeMenu = new Menu("커피", "커피를 선택해주세요");
        coffeeMenu.add(new MenuItem("아메리카노", "진한 에스프레소의 맛과 향을 부드럽게 즐길 수 있는 아메리칸 스타일의 커피입니다.", 4500));
        coffeeMenu.add(new MenuItem("카메라떼", "진한 에스프레소에 우유를 넣어 풍부한 커피향을 부드럽게 즐실 수 있습니다.", 5200));

        // 음식 메뉴
        Menu foodMenu = new Menu("음식", "커피와 함께 즐길 음식을 선택해주세요");
        foodMenu.add(new MenuItem("크로크무슈", "햄과 치즈를 차곡차곡 쌓아 올려 만든 프랑스식 샌드위치로 사르르 녹아내린 모짜렐라 치즈가 입안 가득 풍성한 맛", 4500));
        foodMenu.add(new MenuItem("페스토 햄 파니니", "터키햄과 모짜렐라치즈를 두둑하게 채운 대중적인 파니니", 6500));

        // 메인메뉴에 커피, 음식메뉴를 추가
        mainMenu.add(coffeeMenu);
        mainMenu.add(foodMenu);

        // 전체 메뉴 출력
        mainMenu.print();
    }
메인 메뉴: 메뉴를 선택해주세요
--------------------

커피: 커피를 선택해주세요
--------------------
아메리카노: 진한 에스프레소의 맛과 향을 부드럽게 즐길 수 있는 아메리칸 스타일의 커피입니다. - 4500원
카메라떼: 진한 에스프레소에 우유를 넣어 풍부한 커피향을 부드럽게 즐실 수 있습니다. - 5200원

음식: 커피와 함께 즐길 음식을 선택해주세요
--------------------
크로크무슈: 햄과 치즈를 차곡차곡 쌓아 올려 만든 프랑스식 샌드위치로 사르르 녹아내린 모짜렐라 치즈가 입안 가득 풍성한 맛 - 4500원
페스토 햄 파니니: 터키햄과 모짜렐라치즈를 두둑하게 채운 대중적인 파니니 - 6500원

 

출력결과입니다.

 

 

마치며

복합 패턴은 트리 구조의 객체 구성을 다룰 때 매우 유용한 패턴입니다. 특히 전체-부분 관계를 표현해야 하는 상황에서 강력한 도구가 될 수 있습니다. 하지만 설계가 지나치게 일반화될 수 있다는 점을 주의해야 하며, 실제 요구사항에 맞게 적절히 제약을 추가하는 것이 중요합니다.

 

오류 메세지

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하니 잘 되었습니다.

+ Recent posts