SingleTone

jiyoon·2023년 4월 10일
0

모바일 안드로이드

목록 보기
8/12

싱글톤

싱글톤 패턴은 어떤 클래스가 오직 하나의 인스턴스만을 가질 수 있도록 보장하는 것을 목적으로 한다. 이를 통해 인스턴스의 생성과 소멸을 직접 관리하고, 전역 상태를 유지할 수 있다.
이를 통해 메모리 사용을 최적화하고, 전체 시스템의 성능을 향상시킬 수 있다.

  • 인스턴스 : 클래스를 기반으로 생성된 구체적인 객체이다. 'Person' 클래스로 생성한 'james'객체는 'Person'클래스의 인스턴스가 되는 것이다.

  • 예시 코드

class Singleton {
  constructor() {
    if (!Singleton.instance) {
      Singleton.instance = this;
    }
    return Singleton.instance;
  }
  
  static getInstance() {
    if (!Singleton.instance) {
      Singleton.instance = new Singleton();
    }
    return Singleton.instance;
  }
}
  • 이 코드에서 SingleTone 클래스의 생성자는 instance 변수가 존재하지 않을 때만 this를 SingleTone.instance로 할당한다. 이후 생성자에서 SingleTone.instance를 반환하여, 이미 생성된 객체가 있다면 그 객체를 반환하고, 없다면 새로운 객체를 생성한다. 이를 통해 SingleTone 클래스의 인스턴스는 언제나 하나이며, 여러 개의 인스턴스를 생성하지 않도록 보장한다.

    • this는 객체 자신을 가리키는 예약어이다. 이 코드에서 constructor 내부의 this는 생성된 객체를 가리킨다. constructor를 사용하여 객체를 초기화할 때, 이 객체에 대한 초기값을 this를 사용하여 설정할 수 있다.

활용

리소스 공유

전역적으로 사용되는 리소스를 공유할 때 유용하다. 예를 들어, 로그 객체, 설정 객체, 데이터베이스 연결 객체 등을 싱글톤으로 구현하여 다른 객체에서 쉽게 접근할 수 있게 해준다.

로깅

애플리케이션 전체에서 로그를 수집할 하나의 로그 인스턴스를 사용하여, 중복 로깅을 방지하고 로그를 한 곳에서 관리할 수 있다. 로깅 시스템을 싱글톤으로 구현하여 다른 모듈에서 쉽게 로그를 기록하고, 필요한 경우에 로그 데이터를 조회할 수 있도록 만들 수 있다.

캐시

캐시는 자주 사용되는 데이터를 저장하여 시스템 성능을 향상시키는 데 유용하다. 캐시를 싱글톤으로 구현하여 다른 모듈에서 쉽게 접근할 수 있도록 만들 수 있다.

컨트롤러

컨트롤러는 다른 객체를 제어하거나 시스템 전체의 흐름을 제어하는 데 사용된다. 싱글톤으로 구현된 컨트롤러는 시스템 전체에서 공유되어 사용될 수 있으며, 전체 시스템의 동작을 제어할 수 있다.

연결 풀

데이터베이스나 네트워크 등에서 연결을 맺는 작업은 비용이 큰 작업이다. 연결 풀을 싱글톤으로 구현하여, 연결을 미리 생성해 두고 필요할 때마다 재활용할 수 있다. 이를 통해 시스템 성능을 향상시킬 수 있다.


안드로이드에서의 활용

애플리케이션 상태 관리

안드로이드 애플리케이션에서는 전역적인 상태를 관리해야 하는 경우가 많다. 예를 들어, 애플리케이션의 설정값, 로그인 정보, 사용자 프로필 정보 등을 싱글톤으로 구현하여, 애플리케이션 전체에서 쉽게 접근할 수 있도록 만든다.

public class AppSettings {
    // 싱글톤 인스턴스 변수로 volatile 키워드를 사용하여 멀티스레딩 환경에서 가시성을 보장합니다.
    private static volatile AppSettings instance;

    // SharedPreferences 객체로 애플리케이션 설정값을 저장하고 불러옵니다.
    private SharedPreferences sharedPreferences;

    // 애플리케이션 설정값 변수들
    private String accessToken;
    private String userName;

    // private 생성자를 사용하여 외부에서 인스턴스를 직접 생성할 수 없도록 합니다.
    private AppSettings(Context context) {
        // SharedPreferences 객체를 초기화합니다. "APP_SETTINGS"는 SharedPreferences 파일의 이름입니다.
        sharedPreferences = context.getSharedPreferences("APP_SETTINGS", Context.MODE_PRIVATE);

        // SharedPreferences에서 accessToken과 userName 값을 읽어옵니다.
        // 만약 값이 없다면 기본값으로 null을 사용합니다.
        accessToken = sharedPreferences.getString("access_token", null);
        userName = sharedPreferences.getString("user_name", null);
    }

    // 싱글톤 인스턴스를 얻기 위한 메서드로, 인스턴스가 없으면 새로 생성하고 있으면 기존 인스턴스를 반환합니다.
    public static AppSettings getInstance(Context context) {
        // 인스턴스가 없는 경우
        if (instance == null) {
            // 동기화 블록을 사용하여 인스턴스 생성 과정을 동기화하고,
            // 다른 스레드에서 동시에 인스턴스를 생성하지 못하게 합니다.
            synchronized (AppSettings.class) {
                // 인스턴스가 여전히 없는 경우 새로 생성합니다.
                if (instance == null) {
                    instance = new AppSettings(context.getApplicationContext());
                }
            }
        }
        // 인스턴스를 반환합니다.
        return instance;
    }

    // accessToken getter 메서드
    public String getAccessToken() {
        return accessToken;
    }

    // accessToken setter 메서드
    public void setAccessToken(String accessToken) {
        // 인스턴스 변수에 값을 저장합니다.
        this.accessToken = accessToken;

        // SharedPreferences에 accessToken 값을 저장합니다.
        sharedPreferences.edit().putString("access_token", accessToken).apply();
    }

    // userName getter 메서드
    public String getUserName() {
        return userName;
    }

    // userName setter 메서드
    public void setUserName(String userName) {
        // 인스턴스 변수에 값을 저장합니다.
        this.userName = userName;

        // SharedPreferences에 userName 값을 저장합니다.
        sharedPreferences.edit().putString("user_name", userName).apply();
    }
}



volatile

volatile 키워드는 Java에서 멀티스레딩 환경에서 변수의 가시성과 순서 보장을 위해 사용되는 키워드이다. 변수를 volatile로 선언하면, 해당 변수에 대한 모든 읽기 및 쓰기 작업이 직접 메인 메모리에서 수행된다. 이렇게 함으로써, 각 스레드의 로컬 캐시에서 변수의 값을 가져오지 않고, 항상 메인 메모리에서 가장 최신의 값을 읽어오도록 한다.
싱글톤 패턴에서 volatile 키워드는 이중 검사 잠금(double-checked locking) 패턴에서 인스턴스 변수에 사용된다. 이를 통해 여러 스레드가 동시에 싱글톤 객체를 초기화하지 않도록 하며, 최신의 싱글톤 객체를 모든 스레드에게 보장한다. 이를 통해 멀티스레딩 환경에서의 동시성 문제를 해결할 수 있다.


특징

  • 가시성(Visibility)
    모든 스레드가 volatile 변수의 변경 사항을 실시간으로 확인할 수 있도록 한다. 즉, 한 스레드에서 volatile 변수 값을 변경하면 다른 스레드에서도 그 변경 사항을 즉시 확인할 수 있다.

  • 순서 보장(Ordering)
    volatile 변수에 대한 읽기 및 쓰기 작업이 원자적(atomic)으로 수행된다. 또한 컴파일러와 프로세서가 volatile 변수의 연산 순서를 재정렬하지 않도록 한다. 이렇게 하여 멀티스레딩 환경에서 의도치 않은 동작을 방지할 수 있다.



데이터베이스 연결

안드로이드 애플리케이션에서는 데이터베이스와의 연결을 맺는 작업이 자주 발생한다. 데이터베이스 연결을 싱글톤으로 구현하여, 다른 객체에서 쉽게 데이터베이스에 접근할 수 있도록 만든다.

public class DatabaseManager {
    private static volatile DatabaseManager instance;
    private SQLiteDatabase database;

    // 데이터베이스 생성 또는 오픈
    private DatabaseManager(Context context) {
        DatabaseHelper helper = new DatabaseHelper(context);
        database = helper.getWritableDatabase();
    }

    public static DatabaseManager getInstance(Context context) {
        if (instance == null) {
            synchronized (DatabaseManager.class) {
                if (instance == null) {
                    instance = new DatabaseManager(context.getApplicationContext());
                }
            }
        }
        return instance;
    }

    // 삽입 메서드
    public long insert(String tableName, ContentValues contentValues) {
        return database.insert(tableName, null, contentValues);
    }

    // 조회 메서드
    public Cursor query(String tableName, String[] columns, String selection,
                        String[] selectionArgs, String groupBy, String having,
                        String orderBy) {
        return database.query(tableName, columns, selection, selectionArgs, groupBy, having, orderBy);
    }

    // 수정 메서드
    public int update(String tableName, ContentValues contentValues, String whereClause,
                      String[] whereArgs) {
        return database.update(tableName, contentValues, whereClause, whereArgs);
    }

    // 삭제 메서드
    public int delete(String tableName, String whereClause, String[] whereArgs) {
        return database.delete(tableName, whereClause, whereArgs);
    }
}


네트워크 연결

안드로이드 애플리케이션에서는 서버와의 네트워크 연결을 맺는 작업이 자주 발생한다. 네트워크 연결을 싱글톤으로 구현하여, 다른 객체에서 쉽게 서버와의 통신을 할 수 있도록 만든다.
네트워크 통신을 관리하는 객체, 예를 들어, API 클라이언트나 웹소켓 연결 관리자 등을 싱글톤으로 구현할 수 있다. 이렇게 하면 네트워크 통신을 효율적으로 관리하고 동시성 문제를 줄일 수 있다.

public class ApiClient {
    private static volatile ApiClient instance;
    private Retrofit retrofit;

    private ApiClient() {
        // OkHttpClient 객체를 생성합니다.
        OkHttpClient okHttpClient = new OkHttpClient.Builder().build();

        // Retrofit 객체를 생성하고 기본 URL, HTTP 클라이언트, 변환기를 설정합니다.
        retrofit = new Retrofit.Builder()
                .baseUrl("https://api.example.com") // 기본 URL 설정
                .client(okHttpClient) // HTTP 클라이언트 설정
                .addConverterFactory(GsonConverterFactory.create()) // 응답 데이터를 자동으로 JSON으로 변환해주는 Gson 변환기를 설정
                .build();
    }

    // 싱글톤 패턴을 위한 getInstance() 메서드
    public static ApiClient getInstance() {
        if (instance == null) {
            synchronized (ApiClient.class) {
                if (instance == null) {
                    instance = new ApiClient();
                }
            }
        }
        return instance;
    }

    // 서비스 인터페이스를 생성하는 메서드
    public <T> T createService(Class<T> serviceClass) {
        return retrofit.create(serviceClass);
    }
}
  • 이 예시에서는 Retrofit 라이브러리를 사용하여 네트워크 연결을 처리하고 있다. Retrofit은 RESTful API와 통신하는 데 도움이 되는 라이브러리로, 자동으로 API 요청 및 응답을 Java 인터페이스와 데이터 객체로 변환해준다.


이미지 캐시

안드로이드 애플리케이션에서는 이미지 캐시를 구현하는 데 많은 비용이 소모된다. 이미지 캐시를 싱글톤으로 구현하여, 이미지를 미리 캐싱해 두고 필요할 때마다 재사용할 수 있도록 만든다.

public class ImageCache {

    private static volatile ImageCache instance;
    // LruCache를 사용하여 메모리 캐시를 구현합니다.
    private LruCache<String, Bitmap> memoryCache;

    private ImageCache() {
        // 애플리케이션에서 사용 가능한 최대 메모리를 가져옵니다.
        final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);

        // 캐시 크기를 사용 가능한 메모리의 1/8로 설정합니다.
        final int cacheSize = maxMemory / 8;

        memoryCache = new LruCache<String, Bitmap>(cacheSize) {
            @Override
            protected int sizeOf(String key, Bitmap bitmap) {
                return bitmap.getByteCount() / 1024;
            }
        };
    }

    // 싱글톤 인스턴스를 얻기 위한 메서드로, 인스턴스가 없으면 새로 생성하고 있으면 기존 인스턴스를 반환합니다.
    public static ImageCache getInstance() {
        if (instance == null) {
            synchronized (ImageCache.class) {
                if (instance == null) {
                    instance = new ImageCache();
                }
            }
        }
        return instance;
    }

    // 메모리 캐시에 이미지를 추가하는 메서드
    public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
        if (getBitmapFromMemCache(key) == null) {
            memoryCache.put(key, bitmap);
        }
    }

    // 메모리 캐시에서 이미지를 가져오는 메서드
    public Bitmap getBitmapFromMemCache(String key) {
        return memoryCache.get(key);
    }
}


애플리케이션 로직 관리

안드로이드 애플리케이션에서는 전체적인 애플리케이션 로직을 관리하는 데 많은 비용이 소모된다. 애플리케이션 로직을 싱글톤으로 구현하여, 다른 객체에서 쉽게 애플리케이션 전체의 동작을 제어할 수 있도록 만든다.

public class AppLogic {

    private static volatile AppLogic instance;
    // 애플리케이션 로직 관련 변수들 (예: 사용자 상태, 알림 상태 등)
    private boolean userLoggedIn;
    private boolean notificationsEnabled;

    private AppLogic() {
        // 변수들의 초기값을 설정합니다.
        userLoggedIn = false;
        notificationsEnabled = true;
    }

    // 싱글톤 인스턴스를 얻기 위한 메서드로, 인스턴스가 없으면 새로 생성하고 있으면 기존 인스턴스를 반환합니다.
    public static AppLogic getInstance() {
        if (instance == null) {
            synchronized (AppLogic.class) {
                if (instance == null) {
                    instance = new AppLogic();
                }
            }
        }
        return instance;
    }

    // 사용자 로그인 상태 getter 메서드
    public boolean isUserLoggedIn() {
        return userLoggedIn;
    }

    // 사용자 로그인 상태 setter 메서드
    public void setUserLoggedIn(boolean userLoggedIn) {
        this.userLoggedIn = userLoggedIn;
    }

    // 알림 활성화 상태 getter 메서드
    public boolean isNotificationsEnabled() {
        return notificationsEnabled;
    }

    // 알림 활성화 상태 setter 메서드
    public void setNotificationsEnabled(boolean notificationsEnabled) {
        this.notificationsEnabled = notificationsEnabled;
    }

    // 애플리케이션 로직 관련 메서드들 (예: 로그인, 로그아웃, 알림 관리 등)
    // 여기에 애플리케이션 전체의 동작을 제어하는 메서드들을 구현합니다.
}

애플리케이션 로직 관련 메소드 샘플 코드

    // 로그인 메서드
    public void loginUser(String email, String password) {
        // 로그인 로직을 구현합니다.
        // 예를 들어, 서버에 로그인 요청을 보내고 결과를 받아 처리합니다.
        // 여기서는 간단한 예제로, 이메일과 비밀번호가 일치하는지 확인하고 결과를 반환합니다.

        if (email.equals("example@example.com") && password.equals("password123")) {
            setUserLoggedIn(true);
        } else {
            setUserLoggedIn(false);
        }
    }

    // 로그아웃 메서드
    public void logoutUser() {
        // 로그아웃 로직을 구현합니다.
        // 예를 들어, 로그아웃 상태를 저장하거나 사용자 데이터를 지우는 작업을 수행합니다.
        // 여기서는 간단한 예제로, 사용자 로그인 상태를 false로 변경합니다.

        setUserLoggedIn(false);
    }

    // 알림 활성화/비활성화 토글 메서드
    public void toggleNotifications() {
        // 알림 활성화/비활성화 로직을 구현합니다.
        // 예를 들어, 알림 상태를 저장하거나 알림 설정을 변경하는 작업을 수행합니다.
        // 여기서는 간단한 예제로, 알림 활성화 상태를 반전시킵니다.

        setNotificationsEnabled(!isNotificationsEnabled());
    }


getMenuInflater()는 싱글톤이 어떻게 이루어지는 걸까?

getMenuInflater() 메소드는 안드로이드 시스템에 의해 제공되는 Context 객체를 이용하여 MenuInflater 객체를 생성한다. 이때, getMenuInflater() 메소드는 호출될 때마다 새로운 MenuInflater 객체를 생성하는 것이 아니라, 이미 생성된 MenuInflater 객체를 반환한다. 이를 통해 메모리 사용을 최적화하고, 애플리케이션 전체에서 MenuInflater 객체를 공유하여 사용할 수 있다.

getMenuInflater() 메소드는 현재 액티비티에 대한 MenuInflater 객체를 반환한다. MenuInflater 객체는 메뉴를 인플레이트하는 데 사용된다.

public class MainActivity extends AppCompatActivity {
    LinearLayout baseLayout;
    Button btnChange;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        baseLayout = findViewById(R.id.baseLayout);
        btnChange = findViewById(R.id.btnMenu);
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        MenuInflater menuInflater = getMenuInflater();
        menuInflater.inflate(R.menu.menu1, menu);

        return true;
        //return super.onCreateOptionsMenu(menu);
    }

    @Override
    public boolean onOptionsItemSelected(@NonNull MenuItem item) {

        switch (item.getItemId()) {
            case R.id.subRotate:
                btnChange.setRotation(btnChange.getRotation() + 45);
                break;
            case R.id.subSize:
                btnChange.setScaleX(btnChange.getScaleX() * 2);
                break;
        }
        return false;   //default(정상동작을 막을 경우 true)
        //return super.onOptionsItemSelected(item);
    }
}

그러면 다른 액티비티에서 getMenuInflater() 메소드를 호출하면 그 액티비티의 새로운 Context의 새로운 MenuInflater 객체를 생성해내는 건가?

맞다. 아까도 말했다시피 getMenuInflater() 메소드는 현재 ActivityContext 를 사용하여 MenuInflater 객체를 생성한다. 각 액티비티는 자체 Context를 가지므로, 다른 액티비티에서 getMenuInflater() 메소드를 호출하면 해당 액티비티의 Context를 기반으로 새로운 MenuInflater 객체가 생성되는 것이다.

이렇게 생성된 MenuInflater 객체는 해당 액티비티에서만 사용할 수 있으며, 다른 액티비티에서는 사용할 수 없다. 이는 각 액티비티마다 고유한 Context를 사용하므로, 액티비티에 종속된 리소스를 로드하고 관리할 수 있게 된다.

결론적으로, getMenuInflater() 메소드를 호출하는 액티비티마다 각각의 MenuInflater 객체가 생성되며, 이 객체들은 서로 다른 액티비티에서 사용된다. 이는 안드로이드 애플리케이션에서 액티비티별로 독립적인 옵션 메뉴를 제공하고 관리할 수 있도록 해준다.


profile
주니어 개발자

0개의 댓글