[java] 정적 변수는 왜 악으로 간주됩니까?

저는 회사 세계에 새로운 Java 프로그래머입니다. 최근에 Groovy 와 Java를 사용하여 응용 프로그램을 개발했습니다 . 내가 작성한 코드를 통해 꽤 많은 수의 정적을 사용했습니다. 수석 기술 부지에 사용 된 스태틱 수를 줄이라는 요청을 받았습니다. 나는 똑같은 것을 봤다. 많은 프로그래머가 정적 변수를 사용하는 것에 상당히 반대한다는 것을 알았다.

정적 변수를 사용하는 것이 더 편리하다는 것을 알았습니다. 그리고 클래스 내에서 함수를 10,000 번 호출 해야하는 경우 메소드를 정적으로 만들고 Class.methodCall()대신에 직접 사용하는 것이 좋을 것이기 때문에 그것들도 효율적이라고 생각합니다 (잘못되면 나를 정정하십시오) 클래스의 10,000 개의 인스턴스로 메모리를 어지럽히 는가?

또한 정적은 코드의 다른 부분에 대한 상호 종속성을 줄입니다. 그들은 완벽한 주주 역할을 할 수 있습니다. 이것에 덧붙여 스몰 토크스칼라 와 같은 일부 언어에서는 정적이 널리 구현되어 있습니다 . 그렇다면 왜 프로그래머들 사이에서 (특히 Java 세계에서) 정적에 대한 이러한 억압이 발생합니까?

추신 : 정적에 대한 나의 가정이 틀렸다면 저를 정정하십시오.



답변

정적 변수는 전역 상태를 나타냅니다. 추론하기 어렵고 테스트하기가 어렵습니다. 객체의 새 인스턴스를 만들면 테스트 내에서 새로운 상태에 대해 추론 할 수 있습니다. 정적 변수를 사용하는 코드를 사용하면 모든 상태에있을 수 있으며 무엇이든 수정할 수 있습니다.

나는 꽤 오랫동안 계속할 수 있지만, 더 큰 개념은 생각의 범위가 좁을수록 추론하기가 쉽다는 것입니다. 우리는 작은 것들에 대해 잘 알고 있지만 모듈성이 없다면 백만 라인 시스템의 상태에 대해 추론하기는 어렵습니다. 이것은 정적 변수뿐만 아니라 모든 종류의 것들에 적용됩니다.


답변

객체 지향적이지 않음 :
일부 사람들이 정적을 “악”으로 간주 할 수있는 한 가지 이유는 객체 지향 패러다임 과 반대되는 것입니다. 입니다. 특히, 데이터가 개체 (캡처, 정보 숨기기 등)에 캡슐화되어 있다는 원칙을 위반합니다. 정적을 사용하여 설명하는 방식은 본질적으로 범위와 같은 문제를 처리하지 않기 위해 전역 변수로 사용하는 것입니다. 그러나 전역 변수는 “좋은”객체 지향 코드의 특성이 아닌 절차 적 또는 명령형 프로그래밍 패러다임의 정의 특성 중 하나입니다. 이것은 절차 적 패러다임이 나쁘다는 것은 아니지만 감독자가 “좋은 객체 지향 코드”를 작성하고 실제로 “

항상 명백하지 않은 정적을 사용하기 시작할 때 Java에는 많은 단점이 있습니다. 예를 들어, 동일한 VM에서 두 개의 프로그램 사본이 실행중인 경우 정적 변수의 값을 잘라 내고 서로의 상태를 망칠 수 있습니까? 또는 클래스를 확장하면 어떻게됩니까? 정적 멤버를 재정의 할 수 있습니까? 많은 수의 스태틱이 있고 필요한 다른 인스턴스 객체에 대해 메모리를 회수 할 수 없기 때문에 VM에 메모리가 부족합니까?

객체 수명 :
또한 정적은 프로그램의 전체 런타임과 일치하는 수명을 갖습니다. 즉, 클래스를 사용한 후에도 모든 정적 변수의 메모리를 가비지 수집 할 수 없습니다. 예를 들어, 대신 변수를 비 정적 상태로 만들고 main () 함수에서 클래스의 단일 인스턴스를 만든 다음 10,000 번의 호출이 완료되면 클래스에 특정 함수를 10,000 번 실행하도록 요청한 경우 단일 인스턴스에 대한 참조를 삭제하면 모든 정적 변수가 가비지 수집되어 재사용 될 수 있습니다.

특정 재사용 방지 :
또한 정적 메소드를 사용하여 인터페이스를 구현할 수 없으므로 정적 메소드를 사용하면 특정 객체 지향 기능을 사용할 수 없습니다.

다른 옵션:
효율성이 주요 관심사 인 경우 일반적으로 작성보다 빠른 호출의 이점 만 고려하는 것보다 속도 문제를 해결하는 다른 더 좋은 방법이있을 수 있습니다. 언제 어디서나 과도 또는 휘발성 수정자가 필요한지 고려하십시오. 인라인 기능을 유지하기 위해 메소드를 정적 대신 최종으로 표시 할 수 있습니다. 메소드 매개 변수 및 기타 변수를 최종으로 표시하여 해당 변수를 변경할 수있는 항목에 대한 가정을 기반으로 특정 컴파일러 최적화를 허용 할 수 있습니다. 매번 새 인스턴스를 작성하는 대신 인스턴스 오브젝트를 여러 번 재사용 할 수 있습니다. 앱에 일반적으로 켜져 있어야하는 컴파일러 최적화 스위치가있을 수 있습니다. 아마도 10,000 번의 실행이 다중 스레드가 가능하고 다중 프로세서 코어를 활용할 수 있도록 설계를 설정해야합니다. 만약 portablity가

어떤 이유로 객체의 여러 복사본을 원하지 않으면 단일 디자인 패턴, 스레드 안전성 (단일 코드가 잘 코딩 된 것으로 가정)과 같은 정적 객체에 비해 이점이 있으며, 지연 초기화가 가능하며, 객체가 사용될 때 객체가 올바르게 초기화되었는지 확인, 서브 클래 싱, 코드 테스트 및 리팩토링의 이점, 말할 것도없이, 어느 시점에서 하나의 객체 인스턴스 만 원한다는 생각이 바뀌면 인스턴스 변수를 사용하기 위해 모든 정적 변수 코드를 리팩토링하는 것보다 중복 인스턴스를 방지하기 위해 코드를 제거하는 것이 훨씬 쉽습니다. 전에는 그렇게하지 않았지만 재미 있지 않았으므로 더 많은 클래스를 편집해야하므로 새로운 버그가 발생할 위험이 높아집니다. 처음에 “올바른”설정을하는 것이 훨씬 좋습니다. 그것이 단점이있는 것처럼 보이더라도. 나를 위해 여러 개의 사본이 필요한 길을 결정할 때 필요한 재 작업은 가능한 한 자주 정적을 사용하는 가장 큰 이유 중 하나 일 것입니다. 따라서 정적 함수가 상호 의존성을 줄인다는 귀하의 진술에 동의하지 않을 것입니다. “어떻게 수행해야하는지 알고있는 객체가 아니라 직접 액세스 할 수있는 정적 요소가 많으면 더 많은 코드가 연결될 것입니다.” 그 자체로 “


답변

악은 주관적인 용어입니다.

생성 및 소멸 측면에서 정적을 제어하지 않습니다. 그들은 프로그램 로딩 및 언 로딩에 따라 움직입니다.

정적은 하나의 공간에 존재하므로이를 사용하려는 모든 스레드는 관리해야하는 액세스 제어를 거쳐야합니다. 이것은 프로그램이 더 결합되어 있으며 이러한 변화는 J Skeet과 같이 계획하고 관리하기가 더 어렵다는 것을 의미합니다. 이로 인해 변경 영향을 분리하는 데 문제가 발생하여 테스트 관리 방법에 영향을줍니다.

이것들은 내가 가진 두 가지 주요 문제입니다.


답변

아닙니다. 세계 국가들은 그 자체로 악이 아닙니다. 그러나 우리는 볼 수 있습니다 당신의 당신이 그것을 제대로 사용하면 코드를 볼 수 있습니다. 초보자가 전 세계 국가를 학대 할 가능성이 있습니다. 모든 언어 기능을 남용하는 것처럼 말입니다.

세계 국가는 절대적으로 필요합니다. 우리는 세계 상태를 피할 수 없습니다. 우리는 세계 국가에 대한 추론을 피할 수 없습니다. -응용 프로그램 의미를 이해하려는 경우

이를 위해 세계 국가를 없애려고 노력하는 사람들은 필연적으로 훨씬 더 복잡한 시스템으로 끝납니다. 세계 국가는 여전히 여러 층의 간접적 지시 아래 현명하게 / 아이디 오로 위장되어 있습니다. 우리는 모든 간접 지시를 풀고 난 후에도 여전히 세계 국가에 대해 추론해야합니다.

xml로 멋지게 글로벌 상태를 선언하고 어떻게 든 우수하다고 생각하는 Spring 사람들처럼.

@Jon Skeet은 if I create a new instance of an object이제 객체 내부의 상태와 객체를 호스팅하는 환경의 상태에 대해 두 가지를 고려해야합니다.


답변

정적 변수에는 두 가지 주요 문제가 있습니다.

  • 스레드 안전성-정적 리소스는 정의상 스레드 안전성이 아닙니다.
  • 코드 내 재성-정적 변수가 인스턴스화되는시기와 다른 정적 변수보다 먼저 인스턴스화되는지 여부를 알 수 없음

답변

‘final’키워드없이 ‘static’키워드를 사용하는 경우 디자인을 신중하게 고려해야한다는 신호입니다. 변경 가능한 정적 최종 객체가 위험 할 수 있기 때문에 ‘최종’의 존재조차도 자유 패스가 아닙니다.

나는 ‘최종’이없는 ‘정적’을 볼 때의 약 85 % 어딘가에 추정 할 것입니다. 종종 이러한 문제를 숨기거나 숨기는 이상한 해결 방법을 찾을 수 있습니다.

정적 변수를 만들지 마십시오. 특히 컬렉션. 일반적으로 컬렉션은 포함하는 객체가 초기화 될 때 초기화되어야하며 포함하는 객체를 잊었을 때 재설정되거나 잊혀지도록 설계되어야합니다.

스태틱을 사용하면 매우 미묘한 버그가 생겨 엔지니어에게 고통을 줄 수 있습니다. 나는이 버그를 만들고 사냥했기 때문에 알고 있습니다.

자세한 내용은 다음을 참조하십시오.

왜 정적을 사용하지 않습니까?

테스트 작성 및 실행은 물론 눈에 띄지 않는 미묘한 버그를 포함하여 정적 관련 문제가 많이 있습니다.

정적 객체에 의존하는 코드는 쉽게 단위 테스트를 할 수 없으며 정적을 쉽게 조롱 할 수 없습니다 (보통).

정적을 사용하는 경우 더 높은 수준의 구성 요소를 테스트하기 위해 클래스 구현을 교체 할 수 없습니다. 예를 들어 데이터베이스에서로드하는 Customer 객체를 반환하는 정적 CustomerDAO를 상상해보십시오. 이제 CustomerFilter 클래스가 있는데,이 클래스는 일부 Customer 객체에 액세스해야합니다. CustomerDAO가 정적이면 먼저 데이터베이스를 초기화하고 유용한 정보를 채우지 않고 CustomerFilter에 대한 테스트를 작성할 수 없습니다.

데이터베이스 채우기 및 초기화에 시간이 오래 걸립니다. 내 경험상 DB 초기화 프레임 워크는 시간이 지남에 따라 변경되므로 데이터가 변형되고 테스트가 중단 될 수 있습니다. IE는 고객 1이 VIP 였지만 DB 초기화 프레임 워크가 바뀌었고 이제 고객 1은 더 이상 VIP가 아니라고 테스트했지만 고객 1을로드하도록 테스트를 하드 코딩했습니다.

더 좋은 방법은 CustomerDAO를 인스턴스화하여 생성 될 때 CustomerFilter에 전달하는 것입니다. (더 나은 접근 방식은 Spring 또는 다른 Inversion of Control 프레임 워크를 사용하는 것입니다.

이 작업을 수행하면 CustomerFilterTest에서 대체 DAO를 신속하게 조롱하거나 스터브 아웃 할 수 있으므로 테스트를보다 효과적으로 제어 할 수 있습니다.

정적 DAO가 없으면 테스트가 더 빠르며 (db 초기화 없음) 더 안정적입니다 (db 초기화 코드가 변경 될 때 실패하지 않기 때문에). 예를 들어,이 경우 테스트 1과 관련하여 고객 1은 항상 VIP가됩니다.

테스트 실행

정적은 단위 테스트 세트를 함께 실행할 때 (예 : Continuous Integration 서버에서) 실제 문제를 일으 킵니다. 한 테스트에서 다른 테스트로 열려있는 네트워크 소켓 객체의 정적 맵을 상상해보십시오. 첫 번째 테스트는 포트 8080에서 소켓을 열 수 있지만 테스트가 종료 될 때 맵을 지우는 것을 잊었습니다. 이제 두 번째 테스트가 시작되면 포트가 여전히 점유되어 있기 때문에 포트 8080에 대한 새 소켓을 만들려고 할 때 충돌이 발생할 수 있습니다. 정적 컬렉션의 소켓 참조가 제거되지 않고 WeakHashMap을 제외하고는 가비지 수집 대상이되지 않아 메모리 누수가 발생한다고 상상해보십시오.

이것은 지나치게 일반화 된 예이지만 대규모 시스템에서는이 문제가 항상 발생합니다. 사람들은 단위 테스트가 동일한 JVM에서 소프트웨어를 반복적으로 시작하고 중지하는 것을 생각하지 않지만, 소프트웨어 디자인에 대한 좋은 테스트이며, 고 가용성에 대한 열망이있는 경우이를 알아야합니다.

이러한 문제는 종종 DB 액세스, 캐싱, 메시징 및 로깅 계층과 같은 프레임 워크 개체에서 발생합니다. Java EE 또는 최상의 품종 프레임 워크를 사용하는 경우 아마도이를 위해 많은 것을 관리하지만 레거시 시스템을 다루는 경우 이러한 계층에 액세스하기위한 많은 사용자 정의 프레임 워크가있을 수 있습니다.

이러한 프레임 워크 구성 요소에 적용되는 시스템 구성이 단위 테스트간에 변경되고 단위 테스트 프레임 워크가 구성 요소를 분리 및 재 구축하지 않으면 이러한 변경 사항이 적용되지 않으며 테스트가 해당 변경 사항에 의존하면 실패합니다. .

비 프레임 워크 구성 요소도이 문제의 영향을받습니다. OpenOrders라는 정적지도를 상상해보십시오. 하나의 테스트를 작성하여 미결 주문을 작성하고 모두 올바른 상태인지 확인한 다음 테스트를 종료합니다. 다른 개발자는 필요한 주문을 OpenOrders 맵에 넣는 두 번째 테스트를 작성한 다음 주문 수가 정확하다고 주장합니다. 개별적으로 실행하면 이러한 테스트는 모두 통과되지만 스위트에서 함께 실행하면 실패합니다.

더 나쁜 것은 실패는 테스트가 실행 된 순서에 따라 결정될 수 있습니다.

이 경우 정적을 피함으로써 테스트 인스턴스 전체에서 데이터가 지속될 위험을 피하여 테스트 안정성을 향상시킬 수 있습니다.

미묘한 버그

고 가용성 환경에서 작업하거나 스레드가 시작 및 중지 될 수있는 곳에서 작업하는 경우 코드가 프로덕션 환경에서 실행될 때 유닛 테스트 스위트와 동일한 문제가 적용될 수 있습니다.

정적 객체를 사용하여 데이터를 저장하는 대신 스레드를 처리 할 때는 스레드 시작 단계에서 초기화 된 객체를 사용하는 것이 좋습니다. 이렇게하면 스레드가 시작될 때마다 새로운 잠재적 개체 구성을 가진 객체의 새 인스턴스가 만들어지고 스레드의 한 인스턴스에서 다음 인스턴스로 데이터가 번지는 것을 피할 수 있습니다.

스레드가 죽으면 정적 객체는 재설정되거나 가비지 수집되지 않습니다. “EmailCustomers”라는 스레드가 있고 시작시 정적 문자열 콜렉션을 이메일 주소 목록으로 채우고 각 주소로 이메일을 보내기 시작한다고 가정하십시오. 스레드가 어떻게 든 중단되거나 취소되었다고 가정하면 고 가용성 프레임 워크가 스레드를 다시 시작합니다. 그런 다음 스레드가 시작되면 고객 목록을 다시로드합니다. 그러나 모음은 정적이므로 이전 모음의 전자 메일 주소 목록이 유지 될 수 있습니다. 이제 일부 고객은 이메일이 중복 될 수 있습니다.

제쳐두고 : 정적 결승

기술적 인 구현상의 차이가 있지만 “정적 최종”의 사용은 사실상 C #define과 동등한 Java입니다. AC / C ++ #define은 컴파일 전에 프리 프로세서에 의해 코드에서 교체됩니다. Java “정적 최종”은 스택에 상주하는 메모리를 종료합니다. 이런 식으로 C ++의 “정적 const”변수와 #define보다 유사합니다.

요약

이것이 정적이 문제가되는 몇 가지 기본 이유를 설명하는 데 도움이되기를 바랍니다. Java EE 또는 Spring 등과 같은 최신 Java 프레임 워크를 사용하는 경우 이러한 상황이 많이 발생하지 않을 수 있지만 많은 레거시 코드로 작업하는 경우 훨씬 더 빈번해질 수 있습니다.


답변

아무도 언급하지 않았으므로 동시성. 정적 변수를 읽고 쓰는 스레드가 여러 개인 경우 정적 변수가 놀라 울 수 있습니다. 이것은 웹 응용 프로그램 (예 : ASP.NET)에서 일반적이며 다소 치명적인 버그를 일으킬 수 있습니다. 예를 들어, 페이지에 의해 업데이트되는 정적 변수가 있고 “거의 거의 동시에”두 사람이 페이지를 요청하면 한 사용자가 다른 사용자가 예상 한 결과를 얻거나 더 나빠질 수 있습니다.

statics는 코드의 다른 부분에 대한 상호 종속성을 줄입니다. 그들은 완벽한 국가 보유자 역할을 할 수 있습니다

잠금을 사용하고 경합을 다룰 준비가 되었기를 바랍니다.

* 사실 Preet Sangha가 언급했습니다.